feat(config): add server.external_url for reverse proxy WebAuthn#785
feat(config): add server.external_url for reverse proxy WebAuthn#785
Conversation
Users running moltis behind a reverse proxy with SSL termination (e.g. https://moltis.example.com) had no discoverable way to tell moltis its public URL. The only workaround was setting env vars (MOLTIS_WEBAUTHN_RP_ID + MOLTIS_WEBAUTHN_ORIGIN), which wasn't documented in the config file. Add a first-class `server.external_url` config field that: - Derives WebAuthn RP ID (host) and origin from the URL - Supports MOLTIS_EXTERNAL_URL env var override (highest priority) - Falls through to existing env var chain if unset - Validates scheme (http/https only) and warns on trailing slashes - Is excluded from the generic MOLTIS_ env override mechanism Priority order for WebAuthn RP registration: 1. server.external_url / MOLTIS_EXTERNAL_URL (new) 2. MOLTIS_WEBAUTHN_RP_ID + MOLTIS_WEBAUTHN_ORIGIN (existing) 3. Cloud platform env vars: APP_DOMAIN, RENDER_*, FLY_*, RAILWAY_* 4. Localhost/hostname auto-detection Closes #782 Entire-Checkpoint: 3ae551084ef1
Greptile SummaryThis PR adds Confidence Score: 5/5Safe to merge; the only finding is a documentation wording issue that does not affect runtime behaviour. The empty-host bug from the previous review thread is correctly fixed. All validation, schema-map, template, and test changes are sound. The single remaining finding is a P2 doc clarification in security.md — the fine-grained env-var precedence paragraph describes a use-case that the code does not support, but no runtime breakage results from the wording. docs/src/security.md — the 'take precedence after' phrasing should be corrected to make clear that the per-field WebAuthn env vars are fallbacks used only when
|
| Filename | Overview |
|---|---|
| crates/config/src/schema/system.rs | Adds external_url: Option<String> to ServerConfig, effective_external_url() (reads env > config), and pure resolve_external_url() helper for testability — all well-structured |
| crates/gateway/src/server/prepare_core/post_state.rs | Integrates effective_external_url() into build_webauthn_registry; the empty-host guard added in the fix commit correctly prevents silent passkey loss for hostless URLs |
| crates/config/src/validate/semantic.rs | Adds scheme validation (Error for non-http/https) and trailing-slash warning for server.external_url; consistent with existing validation patterns |
| docs/src/security.md | Adds reverse-proxy passkey section, but the precedence description for fine-grained env vars is backwards relative to the code |
| crates/config/src/schema/tests.rs | Adds 9 tests for external_url field parsing, effective_external_url, and the pure resolve_external_url helper covering precedence, fallback, trailing-slash stripping, and both-unset |
| crates/config/src/validate/schema_map.rs | Correctly adds external_url as a Leaf entry to the server struct in build_schema_map |
| crates/config/src/template.rs | Adds commented-out external_url example with env-var precedence note to the default config template |
| crates/config/src/validate/tests/security.rs | Adds four tests covering bad scheme (error), trailing slash (warning), valid https, and valid http — good coverage of the new validation paths |
| crates/config/src/loader/tests.rs | Adds apply_env_overrides_ignores_external_url test confirming generic env override skips MOLTIS_EXTERNAL_URL, leaving it to effective_external_url() |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[build_webauthn_registry] --> B{effective_external_url\nreturns Some?}
B -- No --> C{Fine-grained env vars\nMOLTIS_WEBAUTHN_RP_ID\nAPP_DOMAIN etc.}
B -- Yes --> D{url::Url::parse OK?}
D -- No --> E[warn! invalid URL\nreturn None,None]
E --> C
D -- Yes --> F{host_str empty?}
F -- Yes --> G[warn! no hostname\nreturn None,None]
G --> C
F -- No --> H[external_rp_id = host\nexternal_origin = url]
H --> I[explicit_rp_id = external_rp_id\nFine-grained env vars skipped]
C --> J{Any RP ID found?}
I --> J
J -- No --> K[Localhost / hostname fallback]
J -- Yes --> L[try_add rp_id + origin\nWebAuthn RP registered]
K --> L
L --> M{any_ok?}
M -- Yes --> N[return SharedWebAuthnRegistry]
M -- No --> O[return None\npasskeys disabled]
Reviews (2): Last reviewed commit: "fix(config): guard empty-host URL and ad..." | Re-trigger Greptile
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Merging this PR will not alter performance
Comparing Footnotes
|
Address PR review feedback:
- Guard against URLs that parse but have no host (e.g. file:///path).
Without this, unwrap_or_default() produced "", which propagated as
Some("") and silently disabled passkey auth by skipping the localhost
fallback. Now emits a warning and returns (None, None) to fall through.
- Extract resolve_external_url() as a pure function testable without
env var mutation. Add 5 unit tests covering: env precedence over
config, empty env fallback, trailing slash stripping from both
sources, and both-unset returns None.
Entire-Checkpoint: e69b85acb7e1
Review feedback addressed in 7f4c46eP1 — Silent passkey loss when URL has no host: Added empty-host guard. URLs like P2 — Missing unit tests for |
|
@greptile review |
Summary
server.external_urlconfig field so users behind a reverse proxy can declaratively set their public URL for WebAuthn passkey authMOLTIS_EXTERNAL_URLenv var takes precedence over the config fieldCloses #782
Validation
Completed
cargo test -p moltis-config— 239 tests pass (13 new)cargo check -p moltis-gateway— compiles cleancargo clippy -p moltis-config -- -D warnings— no warningscargo clippy -p moltis-gateway -- -D warnings— no warningscargo +nightly-2025-11-30 fmt --all -- --check— no new formatting issuesRemaining
./scripts/local-validate.sh— full CI validationexternal_url = "https://test.example.com"in moltis.toml, start gateway, verify log showsWebAuthn RP registeredwith the correct originMOLTIS_EXTERNAL_URL=https://env.example.com, verify it takes priority over config valueManual QA
[server] external_url = "https://test.example.com"— verify startup log showsWebAuthn RP registeredwith RP IDtest.example.comand originhttps://test.example.comMOLTIS_EXTERNAL_URL=https://env.example.comenv var alongside the config field — verify the env var wins (RP ID =env.example.com)external_url = "ftp://bad.example.com"— verifymoltis config validatereports an errorexternal_url = "https://example.com/"— verify validation warns about trailing slash