HTTP/HTTPS reverse proxy routing traffic to Hero Unix sockets with TLS and OAuth.
  • Rust 80.3%
  • JavaScript 11.2%
  • HTML 5.1%
  • CSS 3.4%
Find a file
mik-tf 79dee2f9ca
All checks were successful
lab publish / publish (push) Successful in 30m2s
fix(oauth): complete Forgejo OIDC login (aud array + userinfo enrichment + openid scope)
Forgejo SSO login failed on the callback with "OAuth code exchange failed:
validate id_token". Three stacked bugs in the OIDC path, fixed here:

1. id_token `aud` shape. Forgejo emits `aud` as a single-element JSON array
   (`["<client_id>"]`), but IdTokenClaims.aud was typed `String`, so serde
   deserialization failed before any check ran ("invalid type: sequence,
   expected a string"). `aud` now deserializes from string-or-array per
   RFC 7519 section 4.1.3. The audience membership check is still enforced by
   jsonwebtoken's Validation::set_audience (which parses the raw claim
   independently of our struct), so this does not weaken validation.

2. Identity from userinfo. Forgejo's id_token carries only sub/iss/aud/exp/
   iat/nonce -- never preferred_username/email/name (even with profile/email
   scope); those live solely in the userinfo endpoint. The OIDC path read
   identity only from the id_token, so the local username fell back to the
   numeric `sub` and never matched the username-based access allowlist. After
   validating the id_token, the OIDC path now enriches the identity from
   userinfo, binding the userinfo `sub` to the validated id_token `sub`
   (OIDC Core section 5.3.2). userinfo failure is non-fatal.

3. openid scope durability. The boot-time provider seed defaulted scopes to
   `read:user`, dropping `openid` on a proxy restart so Forgejo stopped
   returning an id_token. The seed now ensures `openid` is present for the
   OIDC provider regardless of the stored scope secret.

Also: the OAuth callback now logs the full error chain ({:#}) and id_token
validation failures include the token's iss/aud vs the required values, so a
mismatch is diagnosable from the proxy log without a second login attempt.

Verified end to end against forge.ourworld.tf SSO: a real login now returns
302 with the username resolved from userinfo. fmt/clippy -D warnings clean,
69 unit tests pass (8 new). An independent security review confirmed the
audience/issuer/signature/nonce checks remain intact and the userinfo
subject-binding is not bypassable.

Fixes #61
Closes #60

Signed-by: mik-tf <mik-tf@noreply.invalid>
2026-06-08 14:28:46 -04:00
.forgejo/workflows ci: build with lab, publish rolling latest/dev musl releases 2026-06-02 16:20:04 +02:00
.hero refactor: migrate from HERO_SOCKET_DIR/HERO_* env vars to PATH_SOCKET + DB-only config 2026-05-26 12:19:34 +02:00
crates fix(oauth): complete Forgejo OIDC login (aud array + userinfo enrichment + openid scope) 2026-06-08 14:28:46 -04:00
docs hero_proxy: seed gateway listener from env 2026-05-26 22:11:07 -04:00
patches build(deps): pin upstream hero_* deps to main (was development) (#58) 2026-06-03 18:54:17 +00:00
schema chore: auto-commit local changes before pull 2026-05-31 23:42:40 +02:00
.gitignore fix: remove hardcoded /Volumes/T7 [patch] section and add .gitignore entry 2026-05-12 15:38:04 +02:00
Cargo.lock fix(proxy): enable jsonwebtoken aws_lc_rs backend so OAuth login does not panic 2026-06-08 13:07:28 -04:00
Cargo.toml build(deps): pin upstream hero_* deps to main (was development) (#58) 2026-06-03 18:54:17 +00:00
PURPOSE.md refactor: migrate from HERO_SOCKET_DIR/HERO_* env vars to PATH_SOCKET + DB-only config 2026-05-26 12:19:34 +02:00
README.md refactor: migrate from HERO_SOCKET_DIR/HERO_* env vars to PATH_SOCKET + DB-only config 2026-05-26 12:19:34 +02:00
rust-toolchain.toml chore: pin toolchain and rust-version to 1.96 2026-06-01 13:35:49 +02:00

Hero Proxy

HTTP/HTTPS reverse proxy and service discovery for the Hero ecosystem. Routes incoming traffic to Hero services via URL-prefix → Unix domain socket forwarding, with TLS termination (self-signed or Let's Encrypt), SSH tunnels, OAuth, and a full management API.

Architecture

Internet / Browser
 │
 :80 (HTTP) / :443 (HTTPS)
 │
 hero_proxy_server
 ├── {domain}/* → ~/hero/var/sockets/{service}/*.sock
 ├── TLS (self-signed or Let's Encrypt / ACME)
 ├── SSH reverse-port tunnels
 ├── OAuth2 / bearer / signature auth per route
 └── OpenRPC JSON-RPC 2.0 management API
 │
 ~/hero/var/sockets/hero_proxy/rpc.sock

Crates

Crate Type Description
hero_proxy binary CLI — registers and starts/stops all services via hero_proc
hero_proxy_server binary TCP proxy + service discovery + OpenRPC API
hero_proxy_admin binary Admin dashboard
hero_proxy_sdk library Auto-generated typed client
weblib library Reusable web / TLS / ACME library

Sockets

All sockets under $PATH_SOCKET/hero_proxy/ (default ~/hero/var/sockets/hero_proxy/).

Socket Protocol Description
rpc.sock OpenRPC / JSON-RPC 2.0 Management API
admin.sock HTTP Admin dashboard

Ports (TCP)

Port Protocol Description
9997 HTTP Proxy ingress (dev / LAN)
9996 HTTPS Proxy ingress (TLS)
443 HTTPS Production ingress (Let's Encrypt)
80 HTTP Redirect → HTTPS (Let's Encrypt mode)

Service Management

All service lifecycle operations are driven by lab on top of hero_proc.

Install and start

lab service proxy --install # build + install all declared binaries
lab service proxy --start # register with hero_proc and start

Stop / restart

lab service proxy --stop
lab service proxy --stop && lab service proxy --start

Status

lab service proxy --status

TLS / ACME Configuration

TLS settings are persisted in the database so they survive restarts without environment variables. DB values always take precedence over env vars.

# Set the domain for Let's Encrypt (required)
proxy system config set --dns-name proxy.example.com

# Set contact email (strongly recommended — expiry notifications)
proxy system config set --acme-email admin@example.com

# Switch to production CA (default is staging — safe to test first)
proxy system config set --acme-production true

# View current effective config and where each value comes from
proxy system config get

# Clear a field (reverts to env var fallback, or unset)
proxy system config remove dns_name

Response from proxy system config get

{
 dns_name: "proxy.example.com"
 acme_email: "admin@example.com"
 acme_production: true
 dns_name_source: "db" # db | env | unset
 acme_email_source: "db" # db | env | unset
 acme_production_source: "db" # db | env | default
}

Env var fallbacks (legacy / alternative)

DB key (via API) Environment variable Default
acme_dns_name HERO_PROXY_DNS_NAME — (self-signed)
acme_email HERO_PROXY_ACME_EMAIL — (no email)
acme_production HERO_PROXY_ACME_PRODUCTION false (staging)

Changes take effect the next time a listener with tls_mode=letsencrypt is started. Running listeners are not hot-reloaded — restart the listener after changing config.


Quick-start: public HTTPS with Let's Encrypt

# 1. Install and start the service
lab service proxy --install
lab service proxy --start

# 2. Configure ACME (while service is running — no restart needed for config)
proxy system config set --dns-name proxy.example.com --acme-email admin@example.com
proxy system config set --acme-production true

# 3. Add an HTTPS listener (starts the ACME challenge immediately)
proxy listener add "0.0.0.0:443" --protocol https --tls-mode letsencrypt

# 4. Verify
proxy listener status
proxy tls check proxy.example.com

Quick-start: SSH tunnel to a public server

# Create HTTP + HTTPS + DNS-bridge tunnels in one call and start them
proxy tunnel quick_add edge.example.com admin --auth-key-path ~/.ssh/id_ed25519

# Check status
proxy tunnel status
proxy tunnel check_dns 3

Domain routing

# Route a hostname to a local service socket
proxy domain add app.example.com socket /run/myapp.sock

# Route with bearer-token auth
proxy domain add api.example.com https https://127.0.0.1:9000 --auth-mode bearer

# Route with OAuth (Google)
proxy oauth set google google $CLIENT_ID $CLIENT_SECRET \
 --scopes "openid email profile"
proxy domain add dash.example.com http http://127.0.0.1:8080 \
 --auth-mode oauth --oauth-provider google

# List / inspect routes
proxy domain list
proxy domain get 3

Development

# Fast check without producing a binary
cargo check -p hero_proxy_server

# Run tests
cargo test

# Clippy
cargo clippy --all-targets -- -D warnings

# Format
cargo fmt

Documentation