Skip to content

Commit 50e7631

Browse files
committed
Allow remote auth over HTTP
1 parent d5896e8 commit 50e7631

6 files changed

Lines changed: 74 additions & 45 deletions

File tree

README.en.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ For a publicly reachable deployment, the current build now includes:
172172
- `HttpOnly` session cookie
173173
- a `24` hour IP block after `3` failed passphrase attempts within `10` minutes
174174
- server-side single-root restrictions via `root.path`
175-
- HTTPS-required login for non-local hosts
175+
- public access over HTTP or HTTPS, with HTTPS reverse proxy still recommended
176176

177177
Deployment details are documented here:
178178

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ pnpm build:cli
171171
- `HttpOnly` session cookie
172172
- 同一 IP `10` 分钟内 `3` 次口令错误后封禁 `24` 小时
173173
- 基于 `root.path` 的服务端单根目录白名单
174-
- 对外访问时要求通过 HTTPS 反向代理提交口令
174+
- 支持通过 HTTP 或 HTTPS 对外访问,推荐在公网入口前使用 HTTPS 反向代理
175175

176176
部署细节请看:
177177

apps/server/src/auth/mod.rs

Lines changed: 54 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -479,7 +479,6 @@ pub(crate) fn login(
479479
if !request.public_mode {
480480
return Ok((status_response(&auth.file, &request, true), String::new()));
481481
}
482-
ensure_transport_allowed(&request)?;
483482
if !password_configured(&auth.file) {
484483
return Err(AuthFailure::new(
485484
StatusCode::SERVICE_UNAVAILABLE,
@@ -599,7 +598,6 @@ pub(crate) fn require_session(
599598
});
600599
}
601600

602-
ensure_transport_allowed(&request)?;
603601
let token = read_cookie_token(headers).ok_or_else(|| {
604602
AuthFailure::new(StatusCode::UNAUTHORIZED, "session_missing").clear_cookie()
605603
})?;
@@ -969,24 +967,14 @@ fn status_response(
969967
authenticated,
970968
password_configured: password_configured(file),
971969
local_host: request.is_local_host,
972-
secure_transport_required: request.public_mode && !request.is_local_host,
970+
secure_transport_required: false,
973971
secure_transport_ok: request.is_local_host || request.is_secure_transport,
974972
session_idle_minutes: file.session_idle_minutes,
975973
session_max_hours: file.session_max_hours,
976974
allowed_roots: file.allowed_roots.clone(),
977975
}
978976
}
979977

980-
fn ensure_transport_allowed(request: &RequestContext) -> Result<(), AuthFailure> {
981-
if request.public_mode && !request.is_local_host && !request.is_secure_transport {
982-
return Err(AuthFailure::new(
983-
StatusCode::BAD_REQUEST,
984-
"secure_transport_required",
985-
));
986-
}
987-
Ok(())
988-
}
989-
990978
fn request_context(
991979
headers: &HeaderMap,
992980
client_addr: SocketAddr,
@@ -1416,4 +1404,57 @@ mod tests {
14161404
assert_eq!(effective_root_path(&file), None);
14171405
assert!(file.allowed_roots.is_empty());
14181406
}
1407+
1408+
#[test]
1409+
fn insecure_remote_public_mode_is_allowed_in_status_response() {
1410+
let file = AuthFile {
1411+
version: 1,
1412+
public_mode: true,
1413+
password: "demo-passphrase".to_string(),
1414+
root_path: "/srv/coder-studio".to_string(),
1415+
allowed_roots: vec!["/srv/coder-studio".to_string()],
1416+
bind_host: DEFAULT_BIND_HOST.to_string(),
1417+
bind_port: DEFAULT_BIND_PORT,
1418+
session_idle_minutes: DEFAULT_SESSION_IDLE_MINUTES,
1419+
session_max_hours: DEFAULT_SESSION_MAX_HOURS,
1420+
sessions: Vec::new(),
1421+
};
1422+
let request = RequestContext {
1423+
ip: "203.0.113.10".to_string(),
1424+
user_agent: "test".to_string(),
1425+
is_local_host: false,
1426+
is_secure_transport: false,
1427+
public_mode: true,
1428+
};
1429+
1430+
let status = status_response(&file, &request, false);
1431+
1432+
assert!(status.public_mode);
1433+
assert!(!status.authenticated);
1434+
assert!(!status.local_host);
1435+
assert!(!status.secure_transport_required);
1436+
assert!(!status.secure_transport_ok);
1437+
}
1438+
1439+
#[test]
1440+
fn remote_http_sessions_use_non_secure_cookies() {
1441+
let expires_at_ms = now_epoch_ms() + 60_000;
1442+
let insecure_request = RequestContext {
1443+
ip: "203.0.113.10".to_string(),
1444+
user_agent: "test".to_string(),
1445+
is_local_host: false,
1446+
is_secure_transport: false,
1447+
public_mode: true,
1448+
};
1449+
let secure_request = RequestContext {
1450+
is_secure_transport: true,
1451+
..insecure_request.clone()
1452+
};
1453+
1454+
let insecure_cookie = build_session_cookie("token", expires_at_ms, &insecure_request);
1455+
let secure_cookie = build_session_cookie("token", expires_at_ms, &secure_request);
1456+
1457+
assert!(!insecure_cookie.contains("; Secure"));
1458+
assert!(secure_cookie.contains("; Secure"));
1459+
}
14191460
}

apps/web/src/components/AuthGate.tsx

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ type AuthGateProps = {
2222
type AuthViewMode =
2323
| "sign-in"
2424
| "not-configured"
25-
| "transport-required"
2625
| "blocked"
2726
| "unavailable";
2827

@@ -310,16 +309,13 @@ export default function AuthGate({ locale, onSelectLocale, children }: AuthGateP
310309
}
311310

312311
const blockedLabel = formatBlockedUntil(blockedUntil, locale);
313-
const securityBlocked = status.secure_transport_required && !status.secure_transport_ok;
314312
const viewMode: AuthViewMode = !status.password_configured || errorCode === "auth_not_configured"
315313
? "not-configured"
316-
: securityBlocked
317-
? "transport-required"
318-
: errorCode === "ip_blocked"
319-
? "blocked"
320-
: errorCode === "auth_unavailable"
321-
? "unavailable"
322-
: "sign-in";
314+
: errorCode === "ip_blocked"
315+
? "blocked"
316+
: errorCode === "auth_unavailable"
317+
? "unavailable"
318+
: "sign-in";
323319

324320
const inlineMessage =
325321
errorCode === "invalid_credentials"
@@ -330,9 +326,7 @@ export default function AuthGate({ locale, onSelectLocale, children }: AuthGateP
330326

331327
const helperText = viewMode === "sign-in"
332328
? t("authAllowedRootsHint")
333-
: viewMode === "transport-required"
334-
? t("authSecureTransportRequired")
335-
: viewMode === "blocked"
329+
: viewMode === "blocked"
336330
? t("authBlockedUntil", { time: blockedLabel || "—" })
337331
: viewMode === "unavailable"
338332
? t("authUnavailable")
@@ -354,11 +348,6 @@ export default function AuthGate({ locale, onSelectLocale, children }: AuthGateP
354348
statusDescription = t("authNotConfiguredDescription");
355349
statusTone = "warning";
356350
showRetry = true;
357-
} else if (viewMode === "transport-required") {
358-
statusTitle = t("authTransportRequiredTitle");
359-
statusDescription = t("authTransportRequiredDescription");
360-
statusTone = "warning";
361-
showRetry = true;
362351
} else if (viewMode === "blocked") {
363352
statusTitle = t("authBlockedTitle");
364353
statusDescription = t("authBlockedDescription", { time: blockedLabel || "—" });
@@ -416,7 +405,6 @@ export default function AuthGate({ locale, onSelectLocale, children }: AuthGateP
416405
<div className="auth-card-bar">
417406
<div className="auth-badge-stack">
418407
<span className="section-kicker">{t("authPublicModeBadge")}</span>
419-
{viewMode === "transport-required" ? <span className="auth-state-pill warning">HTTPS</span> : null}
420408
{viewMode === "not-configured" ? <span className="auth-state-pill warning">auth.json</span> : null}
421409
{viewMode === "blocked" ? <span className="auth-state-pill danger">IP</span> : null}
422410
</div>

docs/deployment/README.en.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ The current code supports:
1313
- a `24` hour IP ban after `3` failed passphrase attempts within `10` minutes
1414
- server-side single-root restrictions through `rootPath`
1515
- authentication on both `HTTP RPC` and `WebSocket`
16-
- HTTPS-required passphrase submission for non-local hosts
16+
- passphrase submission over either HTTP or HTTPS for non-local hosts
1717
- local access on `localhost`, `127.0.0.1`, and `::1` defaults to non-public mode
1818
- local access with `?auth=force` explicitly forces public mode
1919

@@ -28,7 +28,7 @@ Recommended structure:
2828
Why this is recommended:
2929

3030
- the application process does not terminate TLS itself
31-
- non-local login requires secure transport
31+
- HTTPS avoids sending the passphrase and session cookie over a clear-text network path
3232
- a reverse proxy is the right place for certificates, domains, and public ingress
3333

3434
## Configuration File
@@ -91,13 +91,13 @@ Recommended production values:
9191
That means:
9292

9393
- the app only listens locally
94-
- all public traffic must come through an HTTPS reverse proxy
94+
- public traffic is easier to control through a reverse proxy
9595

9696
If you explicitly want the process to listen beyond loopback, you can set:
9797

9898
- `bindHost`: `0.0.0.0`
9999

100-
But even then, public access should still sit behind an HTTPS reverse proxy because the app does not provide TLS directly.
100+
But even then, public access should still sit behind an HTTPS reverse proxy because the app does not provide TLS directly, and plain HTTP would expose the passphrase plus a non-`Secure` session cookie on the wire.
101101

102102
## Reverse Proxy Requirements
103103

@@ -108,7 +108,7 @@ Your proxy should forward:
108108
- `X-Forwarded-For`
109109
- `X-Forwarded-Proto`
110110

111-
`X-Forwarded-Proto=https` is especially important because the app uses it to decide whether secure transport is in place.
111+
If the proxy terminates HTTPS, keep forwarding `X-Forwarded-Proto=https` so the runtime can mark the returned session cookie as `Secure`.
112112

113113
## Caddy Example
114114

@@ -150,7 +150,7 @@ server {
150150
3. Set the access passphrase
151151
4. Override `rootPath`, `bindHost`, or `bindPort` only if you need non-default values
152152
5. Start or restart the app
153-
6. Configure an HTTPS reverse proxy to `bindHost:bindPort`
153+
6. Configure a reverse proxy to `bindHost:bindPort` as needed, with HTTPS still recommended for public ingress
154154
7. Open your domain and verify the login screen appears first
155155

156156
Minimal setup:

docs/deployment/README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
- 同一 IP 在 `10` 分钟内连续 `3` 次口令错误后,封禁 `24` 小时
1414
- `rootPath` 服务端单根目录白名单
1515
- `HTTP RPC``WebSocket` 同时鉴权
16-
- 非本地访问时要求通过 HTTPS 提交口令
16+
- 非本地访问时允许通过 HTTP 或 HTTPS 提交口令
1717
- `localhost` / `127.0.0.1` / `::1` 访问时默认走非 public mode
1818
- 本地访问时如果 URL 带 `?auth=force`,则强制走 public mode
1919

@@ -28,7 +28,7 @@
2828
推荐这样做的原因:
2929

3030
- 应用进程本身不做 TLS 终止
31-
- 对外登录默认要求安全传输
31+
- HTTPS 入口可以避免口令和 session cookie 以明文经过网络
3232
- 反向代理更适合处理证书、域名和公开入口
3333

3434
## 配置文件
@@ -91,13 +91,13 @@
9191
这表示:
9292

9393
- 应用只监听本机
94-
- 对外流量必须先经过 HTTPS 反向代理
94+
- 对外流量更适合先经过反向代理
9595

9696
如果你明确知道自己在做什么,也可以改成:
9797

9898
- `bindHost`: `0.0.0.0`
9999

100-
但即使这样,公网访问也仍然建议放在 HTTPS 反向代理后面,因为应用本身不直接提供 TLS。
100+
但即使这样,公网访问也仍然建议放在 HTTPS 反向代理后面,因为应用本身不直接提供 TLS,而且 HTTP 会让口令和非 `Secure` cookie 暴露在明文链路上
101101

102102
## 反向代理要求
103103

@@ -108,7 +108,7 @@
108108
- `X-Forwarded-For`
109109
- `X-Forwarded-Proto`
110110

111-
其中 `X-Forwarded-Proto=https` 很关键,因为应用会用它判断当前是否为安全传输
111+
如果代理层已经做了 HTTPS,建议继续透传 `X-Forwarded-Proto=https`,这样运行时返回的 session cookie 会自动带上 `Secure` 属性
112112

113113
## Caddy 示例
114114

@@ -150,7 +150,7 @@ server {
150150
3. 设置访问口令
151151
4. 如有需要,再覆盖默认的 `rootPath``bindHost``bindPort`
152152
5. 启动或重启应用
153-
6. 配置 HTTPS 反向代理到 `bindHost:bindPort`
153+
6. 按需要配置反向代理到 `bindHost:bindPort`,公网入口推荐使用 HTTPS
154154
7. 打开你的域名,确认先出现登录页
155155

156156
最小可用配置:

0 commit comments

Comments
 (0)