Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
281a7da
docs(sso): design spec for the PMOVES SSO gateway (auth once, access …
POWERFULMOVES Jul 25, 2026
1706949
docs(sso): implementation plan (11 tasks) for the SSO gateway
POWERFULMOVES Jul 25, 2026
82c41ca
fix(plan): lazy settings proxy — no import-time env read (Task 1 test…
POWERFULMOVES Jul 25, 2026
f5300da
feat(sso-auth): scaffold service + Supabase-JWT verify + /auth/verify
POWERFULMOVES Jul 25, 2026
a68a0d3
fix(plan): Task 1 security — require role=authenticated + decouple ve…
POWERFULMOVES Jul 25, 2026
6d65c1b
fix(sso-auth): require role=authenticated + decouple verify from OIDC…
POWERFULMOVES Jul 25, 2026
2bc407f
fix(plan): Task 2/3 test correctness — role claim, TestClient host, deps
POWERFULMOVES Jul 25, 2026
9c070b1
feat(sso-auth): GoTrue login (GitHub + email/pw), cookie issuance, lo…
POWERFULMOVES Jul 25, 2026
8c0e725
fix(plan): Task 2/3 tests use autouse fixture, not module-level reload
POWERFULMOVES Jul 25, 2026
f1f10f2
fix(sso-auth): test_login uses autouse env fixture (no module-reload …
POWERFULMOVES Jul 25, 2026
e81baa3
fix(plan): Task 2 open-redirect guard on rd param (_safe_rd)
POWERFULMOVES Jul 25, 2026
c621396
fix(sso-auth): open-redirect guard on rd param (_safe_rd)
POWERFULMOVES Jul 25, 2026
eb3fbe2
fix(plan): Task 2 consolidated review fixes (callback error, e-param,…
POWERFULMOVES Jul 25, 2026
aa96666
fix(sso-auth): Task 2 review fixes — callback error handling, logout …
POWERFULMOVES Jul 25, 2026
9824a54
fix(plan): Task 2 gotrue transport-error fail-closed + outage tests
POWERFULMOVES Jul 25, 2026
0d91d80
fix(sso-auth): harden _safe_rd (backslash/control-char/scheme reject)…
POWERFULMOVES Jul 25, 2026
8e82e95
feat(sso-auth): minimal OIDC subset for jellyfin-plugin-oidc
POWERFULMOVES Jul 25, 2026
c4cdba7
fix(plan): Task 3 OIDC redirect_uri allowlist (HIGH open-redirect fix)
POWERFULMOVES Jul 25, 2026
811d87a
fix(sso-auth): OIDC redirect_uri allowlist + constant-time client-sec…
POWERFULMOVES Jul 25, 2026
f6a5539
fix(plan): config redirect_uris field needs NoDecode (pydantic-settin…
POWERFULMOVES Jul 25, 2026
a4b71bb
feat(sso): compose layer — sso-auth service, Traefik forward-auth edg…
POWERFULMOVES Jul 25, 2026
2a20ca6
feat(sso-auth): Dockerfile (fleet-pinned python:3.11-slim base, non-r…
POWERFULMOVES Jul 25, 2026
9ef3061
fix(plan): sso-auth needs traefik.enable labels or auth router never …
POWERFULMOVES Jul 25, 2026
819db40
fix(sso): register sso-auth with Traefik (enable+network+port) so aut…
POWERFULMOVES Jul 25, 2026
d8b4ef5
feat(sso): env.shared.example — SSO gateway config keys (Task 10)
POWERFULMOVES Jul 25, 2026
35b5ec8
fix(sso-auth): urlencode rd on OIDC no-session bounce (final-review F1)
POWERFULMOVES Jul 25, 2026
f8f20d3
chore(compose): regenerate split overlays to clear drift gate
POWERFULMOVES Jul 25, 2026
def4bb1
Merge branch 'main' into feat/sso-gateway
POWERFULMOVES Jul 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,215 changes: 1,215 additions & 0 deletions docs/superpowers/plans/2026-07-24-pmoves-sso-gateway.md

Large diffs are not rendered by default.

103 changes: 103 additions & 0 deletions docs/superpowers/specs/2026-07-24-pmoves-sso-gateway-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# PMOVES SSO Gateway — Design Spec

**Date:** 2026-07-24
**Goal:** A user who authenticates once against Supabase GoTrue reaches wger (Health), Firefly III (Wealth), Open-Notebook, and Jellyfin **already logged in** — "auth once, access all." Supabase stays the single identity provider; the Supabase `JWT_SECRET` and BoTZ's shipped `validate_jwt` pattern are reused.

**Approved decisions (brainstorm 2026-07-24):**
- Login: **GitHub OAuth (primary) + email/password (fallback)**, both via GoTrue.
- Validator: **custom FastAPI forward-auth service** (`pmoves-sso-auth`), reusing BoTZ's `validate_jwt`.
- Reverse proxy: **Traefik** with the ForwardAuth middleware.
- Routing: **subdomains** (`health/wealth/notebook/media.pmoves.ai`); session cookie scoped to `.pmoves.ai`.
- Scope: **all 4 apps in Phase 1** (Jellyfin included via `jellyfin-plugin-sso` against minimal OIDC endpoints on the auth service).

## Architecture

```
Browser ──HTTPS──> Traefik (edge, pmoves_external, TLS)
│ Host: {health,wealth,notebook,media}.pmoves.ai
ForwardAuth middleware ──subrequest──> pmoves-sso-auth (FastAPI) GET /auth/verify
• no/invalid pmoves_session cookie → 302 https://auth.pmoves.ai/login?rd=<orig>
• valid cookie → 200 + headers: Remote-User, X-Auth-Email, X-Auth-Subject
▼ (on 200, Traefik forwards to the app with those headers injected)
App (wger | firefly | open-notebook | jellyfin) trusts the injected identity
```
Comment on lines +15 to +24

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Exclude Jellyfin from the ForwardAuth path in this diagram.

The diagram places media.pmoves.ai behind ForwardAuth, conflicting with the OIDC-only Jellyfin design and the plan’s explicit “NO forward-auth middleware” instruction. Show Jellyfin routing directly to its public OIDC-capable endpoint.

🧰 Tools
🪛 markdownlint-cli2 (0.23.0)

[warning] 15-15: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/superpowers/specs/2026-07-24-pmoves-sso-gateway-design.md` around lines
15 - 24, Update the architecture diagram’s host list and routing flow to remove
media.pmoves.ai and Jellyfin from the ForwardAuth path. Show Jellyfin routing
directly to its public OIDC-capable endpoint, while keeping the existing
ForwardAuth flow for health, wealth, notebook, and the other listed
applications.


Login/session issuance (also on `auth.pmoves.ai`, same service):
```
GET /login → page: "Sign in with GitHub" + email/password form
GitHub → 302 GoTrue /authorize?provider=github&redirect_to=/callback
email/pw→ POST /login → GoTrue /auth/v1/token?grant_type=password
GET /callback → exchange GoTrue code/token → Set-Cookie pmoves_session=<supabase access_jwt>
(HttpOnly, Secure, SameSite=Lax, Domain=.pmoves.ai) → 302 back to rd
GET /logout → clear cookie → 302 GoTrue logout
```

## Components

### 1. `pmoves-sso-auth` (new service — `pmoves/services/sso-auth/`)
FastAPI app. Responsibilities and endpoints:
- **`GET /auth/verify`** — Traefik ForwardAuth target. Reads `pmoves_session` cookie, runs `validate_jwt(cookie, SUPABASE_JWT_SECRET, alg=HS256)` (lifted from `PMOVES-BoTZ/features/mcp_bridge/auth.py`). On success → `200` with response headers `Remote-User` (GoTrue `email` or `sub`), `X-Auth-Email` (email), `X-Auth-Subject` (`sub`). On failure/absent → `401` (Traefik converts to the configured redirect). Near-zero latency; no network calls on the hot path (HS256 local verify).
- **`GET /login`** — renders login page (GitHub button + email/pw form), carries `rd` (return destination).
- **`POST /login`** — email/pw → GoTrue `/auth/v1/token?grant_type=password` → set cookie → redirect `rd`.
- **`GET /callback`** — GoTrue OAuth callback (GitHub) → obtain session → set cookie → redirect `rd`.
- **`GET /logout`** — clear cookie, GoTrue signout.
- **`GET /healthz`** — liveness.
- **OIDC-for-Jellyfin subset** (only consumed by `jellyfin-plugin-sso`): `GET /.well-known/openid-configuration`, `GET /oidc/authorize` (reuses the same GoTrue login → issues an OIDC `code`), `POST /oidc/token` (returns an `id_token` minted from the validated Supabase identity), `GET /oidc/userinfo`. This is a *thin adapter over the already-validated session*, not a general OIDC provider — scoped to Jellyfin's client_id only.

Config (env, from the pipeline): `SUPABASE_JWT_SECRET` (alias of `JWT_SECRET`), `GOTRUE_URL` (internal `http://supabase-gotrue:9999` / external `https://supabase.pmoves.ai`), `SSO_COOKIE_DOMAIN=.pmoves.ai`, `SSO_SESSION_TTL`, `JELLYFIN_OIDC_CLIENT_ID`/`_SECRET`. No new secrets minted — reuses `JWT_SECRET`.

### 2. Traefik (`pmoves-traefik` — `pmoves/docker-compose.traefik.yml`)
- Edge on `pmoves_external`; entrypoints `:80`→`:443` redirect. TLS via **Cloudflare DNS-01** for `*.pmoves.ai` (Cloudflare is the existing DNS per the fleet setup); mesh-internal access (Tailscale/Headscale hostnames) is the reachability path, DNS-01 is the cert path.
- One router per app (Host rule → app service), all attached to the shared `forward-auth@docker` middleware pointing at `pmoves-sso-auth:8080/auth/verify`, `authResponseHeaders: Remote-User,X-Auth-Email,X-Auth-Subject`.
- `auth.pmoves.ai` router → `pmoves-sso-auth` **without** the ForwardAuth middleware (login must be reachable unauthenticated).
- Apps drop their host `ports:` — reachable only through Traefik.

### 3. Per-app integration
| App | Mechanism | Change |
|---|---|---|
| **firefly** (Laravel) | `AUTHENTICATION_GUARD=remote_user_guard`, `AUTHENTICATION_GUARD_HEADER=Remote-User`, `AUTHENTICATION_GUARD_EMAIL=X-Auth-Email` | compose env only; auto-provisions the user on first header-auth. **Firefly has NO native OIDC** (open FR #10662) — remote-user IS the documented SSO path. **Disable Firefly MFA** when remote-user is on. |
| **wger** (Django) | `RemoteUserMiddleware` in `MIDDLEWARE`, `REMOTE_USER` trust; `WGER_ALLOW_REMOTE_USER=True` in the PMOVES wger fork | fork config + compose env |
| **open-notebook** (Streamlit fork) | replace `PasswordAuthMiddleware` with `RemoteUserMiddleware` in `PMOVES-Open-Notebook/api/auth.py` — trust `Remote-User` (fallback to the existing token for non-proxied access) | fork code. **Confirmed password-only, no OIDC/SSO upstream** — header-trust is the only option. |
| **jellyfin** | install an OIDC SSO plugin, configured against the auth service's OIDC subset; auto-provision Jellyfin users from the Supabase `sub`/email | plugin baked into the PMOVES jellyfin fork image at a PINNED version. **Primary: `Ezeqielle/jellyfin-plugin-oidc`** (actively maintained — v1.0.8 2026-07-15, pushed 2026-07-23; generic-OIDC). **Fallback: `9p4/jellyfin-plugin-sso`** (archived 2026-05-12 but battle-tested, 1461★, generic-OIDC, ≥10.8) if Ezeqielle proves too thin. Jellyfin has NO native OIDC (open FR #16470) and no trusted-header auth, so an OIDC plugin is the only path. **Caveat:** OIDC plugins cover the **web UI only** — Jellyfin *mobile apps* still use native login (out of scope for browser-seamless Phase 1). |

**Current-docs validation (2026-07-24):** the header-based forward-auth is the *correct common denominator*, not a shortcut — Firefly III and Open-Notebook have **no OIDC at all**, so trusted-header is their only SSO; wger uses Django's `RemoteUserMiddleware`; only Jellyfin requires OIDC (hence the auth-service OIDC subset). No simpler unified solution exists given this heterogeneity. Sources: Firefly III authentication docs + FRs #10662/#6514; open-notebook README (password-only); jellyfin-plugin-sso (archived, generic-OIDC).

Only the reverse proxy is trusted to set `Remote-User`; apps must reject the header on any path not fronted by Traefik (defence-in-depth — bind app ports to the Traefik network only).

## Data flow — session lifecycle
1. Unauthenticated request to `health.pmoves.ai` → Traefik ForwardAuth → `/auth/verify` 401 → 302 to `auth.pmoves.ai/login?rd=health.pmoves.ai`.
2. User signs in (GitHub or email/pw) → GoTrue issues Supabase JWT → cookie `pmoves_session` set on `.pmoves.ai`.
3. Redirect back → ForwardAuth now `200` + `Remote-User` → app auto-logs-in.
4. Same cookie satisfies `wealth/notebook/media.pmoves.ai` — no re-login (that IS the SSO).
5. JWT ~1h; **Phase 1:** on expiry `/auth/verify` returns 401 → transparent re-login (GoTrue session is still valid, so the redirect round-trip is invisible when the GoTrue cookie persists). **Enhancement (out of Phase 1):** silent refresh via a second HttpOnly `pmoves_refresh` cookie holding the GoTrue refresh token, exchanged in `/auth/verify` before the access JWT expires.

## Error handling
- Expired/invalid JWT → 401 → redirect to login (never a 500).
- GoTrue unreachable → login page shows a clear error; `/auth/verify` fails-closed (401) so apps stay protected.
- App up but header missing (misconfig) → app denies; Traefik logs the missing `Remote-User`.
- Jellyfin OIDC failure → falls back to Jellyfin's native login (not locked out).

## Security
- Cookie: HttpOnly, Secure, SameSite=Lax, `Domain=.pmoves.ai`.
- Apps trust `Remote-User` **only** from Traefik — app containers do not publish host ports; only Traefik is on their network edge.
- Reuse the single `SUPABASE_JWT_SECRET`; no new long-lived secret. HS256 today; the asymmetric-JWT (JWKS) migration (separate design) later swaps the verify path only.
- Forward-auth is fail-closed.

## Testing
- Unit: `/auth/verify` with valid / expired / tampered / absent cookie → 200+headers / 401.
- Integration: login (both methods) sets a valid cookie; a second app request with the cookie returns 200 without re-login.
- Per-app: header-auth auto-provisions + logs in the user (firefly, wger, open-notebook); Jellyfin OIDC round-trip creates + logs in the user.
- Negative: direct app access bypassing Traefik with a forged `Remote-User` is rejected.

## Scope / phasing
**Phase 1 (this spec):** Traefik + `pmoves-sso-auth` + all 4 apps (wger, firefly, open-notebook header-auth; jellyfin via OIDC subset). Delivers true one-login-all-apps.
**Out of scope (separate specs):** asymmetric JWKS migration; 2FA; adding further apps; the firefly tmpfs-hardening 500 fix (tracked separately, unblocks firefly serving at all).

## Global constraints
- Supabase GoTrue is the ONLY IdP; reuse `JWT_SECRET`/`SUPABASE_JWT_SECRET` (no new IdP, no separate user store).
- Reuse BoTZ's `validate_jwt` (HS256) — do not reimplement JWT verification.
- Apps are PMOVES forks — prefer config/env over code; code changes only where no header-auth config exists (open-notebook middleware).
- Follow the env pipeline (example → manifest → funnel); never inline secrets.
- Compose/Dockerfile/migration edits go through their Known-Road domains.
- Prerequisite: firefly must actually serve (its tmpfs 500 fix) before its SSO can be validated end-to-end.
13 changes: 13 additions & 0 deletions pmoves/config/traefik/dynamic.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
http:
middlewares:
pmoves-forward-auth:
forwardAuth:
address: "http://pmoves-sso-auth:8080/auth/verify"
authResponseHeaders: ["Remote-User", "X-Auth-Email", "X-Auth-Subject"]
# On 401 Traefik returns the auth service's response; a 401 body/redirect
# sends the browser to auth.pmoves.ai/login (see errors middleware below).
pmoves-auth-redirect:
errors:
status: ["401"]
service: sso-auth@docker
query: "/login?rd=https://{host}{uri}"
Comment on lines +9 to +13

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Redirect unauthenticated browsers to the auth host

When forward-auth returns 401 for Health, Wealth, or Notebook, Traefik's errors middleware fetches this query internally and substitutes the response; it does not navigate the browser to auth.pmoves.ai. Because /login returns HTML with a relative /login form action, the browser remains on the protected app host and submits through the same forward-auth router again, so an unauthenticated user cannot complete login; return an actual redirect to the auth host instead.

Useful? React with 👍 / 👎.

12 changes: 10 additions & 2 deletions pmoves/docker-compose.agents.yml
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,12 @@ services:
restart: unless-stopped
environment:
# SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY from env.tier-agent
# NATS_URL from env.tier-agent has credentials: nats://nats:***@nats:4222
# NATS_URL pinned to the in-network broker (documented default creds —
# feedback_nats_creds_convention). Two prior sources delivered a
# HOST-shaped value (localhost) into the container: env.tier-agent after
# a prod-bundle materialization, and a duplicate ${NATS_URL} entry below
# (last list entry wins). Broke archon's broker connect 2026-07-24.
- NATS_URL=${ARCHON_BUS_NATS_URL:-nats://nats:pmoves@nats:4222}
Comment on lines +194 to +199

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Remove the committed NATS credential fallback.

Line 199 hardcodes nats:pmoves. If ARCHON_BUS_NATS_URL is unset, Archon uses a known credential or diverges from the broker credentials configured through NATS_USER/NATS_PASSWORD in pmoves/docker-compose.yml. Source the complete URL through the environment/secret mechanism and fail fast when it is unavailable.

As per path instructions, committed Compose configuration must keep secrets out of configuration files. Based on learnings, agent NATS settings should use the established environment/secret wiring rather than a credentialed default.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pmoves/docker-compose.agents.yml` around lines 194 - 199, Remove the
credentialed fallback from NATS_URL in the Compose configuration. Source the
complete broker URL through the established environment/secret wiring, require
ARCHON_BUS_NATS_URL to be provided, and fail fast when it is unset; do not
introduce any hardcoded username or password.

Sources: Path instructions, Learnings

- PORT=8091
- ENVIRONMENT=${ENVIRONMENT:-production}
- ARCHON_ENV=${ARCHON_ENV:-production}
Expand All @@ -209,7 +214,10 @@ services:
- SUPABASE_REST_URL=${ARCHON_SUPABASE_REST_URL:-${SUPABASE_REST_URL:-http://supabase-kong:8000/rest/v1}}
- SUPABASE_SERVICE_ROLE_KEY=${SUPABASE_SERVICE_ROLE_KEY:-${SERVICE_ROLE_KEY:-${SUPABASE_SECRET_KEY:-}}}
- SUPABASE_SERVICE_KEY=${SUPABASE_SERVICE_KEY:-${SUPABASE_SERVICE_ROLE_KEY:-${SERVICE_ROLE_KEY:-${SUPABASE_SECRET_KEY:-}}}}
- NATS_URL=${NATS_URL}
# (duplicate NATS_URL removed 2026-07-24: ${NATS_URL} interpolates the
# HOST-shaped project value (localhost) into the container — last list
# entry wins, silently overriding the in-network pin above. Same class
# as the nats-init localhost bug.)
- SUPA_REST_URL=${ARCHON_SUPABASE_REST_URL:-${SUPABASE_REST_URL:-http://supabase-kong:8000/rest/v1}}
# TensorZero routing — _sync_openai_compat_env() in services/archon/main.py
# reads TENSORZERO_BASE_URL and populates OPENAI_COMPATIBLE_BASE_URL* vars
Expand Down
60 changes: 51 additions & 9 deletions pmoves/docker-compose.external.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ services:
DJANGO_CLEAR_STATIC_FIRST: "${WGER_CLEAR_STATIC_FIRST:-True}"
TIME_ZONE: "${WGER_TIME_ZONE:-America/New_York}"
SITE_URL: "${WGER_SITE_URL:-http://localhost:8010}"
# SSO gateway: trust the Remote-User header injected by Traefik forward-auth.
# wger's nginx front (wger-nginx) must proxy_set_header Remote-User upstream.
WGER_ALLOW_REMOTE_USER: "True"
WGER_REMOTE_USER_HEADER: "Remote-User"
volumes:
- wger-static:/home/wger/static
- wger-media:/home/wger/media
Expand All @@ -30,8 +34,16 @@ services:
aliases:
- pmoves-wger-nginx
pmoves_external: {}
ports:
- "${WGER_HOST_PORT:-8010}:80"
# No host `ports:` — reachable only through Traefik (health.pmoves.ai), so the
# forward-auth Remote-User header cannot be spoofed by bypassing the proxy.
Comment on lines +37 to +38

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Include the edge overlays in up-external

The supported make -C pmoves up-external path still defines EXTERNAL_DC with only docker-compose.external.yml and starts no Traefik or SSO service, while this change removes every external app's host port. Consequently the target reports success but leaves Wger, Firefly, Open Notebook, and Jellyfin unreachable; include the new edge/auth compose files in that lifecycle or retain a reachable path.

AGENTS.md reference: pmoves/AGENTS.md:L169-L180

Useful? React with 👍 / 👎.

labels:
- traefik.enable=true
- traefik.docker.network=pmoves_external
- traefik.http.routers.health.rule=Host(`health.pmoves.ai`)
- traefik.http.routers.health.entrypoints=websecure
- traefik.http.routers.health.tls.certresolver=cf
- traefik.http.routers.health.middlewares=pmoves-forward-auth@file,pmoves-auth-redirect@file
- traefik.http.services.health.loadbalancer.server.port=80
volumes:
- wger-static:/wger/static:ro
- wger-media:/wger/media:ro
Expand All @@ -56,8 +68,22 @@ services:
TZ: ${FIREFLY_TZ:-America/New_York}
# Neutralise Windows SSL_CERT_FILE leak
SSL_CERT_FILE: ""
ports:
- "${FIREFLY_PORT:-8075}:8080" # Default 8075; 8082 reserved for media-audio in docker-compose.yml
# SSO gateway: Firefly III has NO native OIDC (FR #10662) — remote_user_guard
# IS the documented SSO path. Auto-provisions the X-Auth-Email user on first
# header-auth. DISABLE_FRAME_HEADER lets it render behind the proxy.
AUTHENTICATION_GUARD: remote_user_guard
AUTHENTICATION_GUARD_HEADER: Remote-User
AUTHENTICATION_GUARD_EMAIL: X-Auth-Email
DISABLE_FRAME_HEADER: "true"
# No host `ports:` — reachable only through Traefik (wealth.pmoves.ai).
labels:
- traefik.enable=true
- traefik.docker.network=pmoves_external
- traefik.http.routers.wealth.rule=Host(`wealth.pmoves.ai`)
- traefik.http.routers.wealth.entrypoints=websecure
- traefik.http.routers.wealth.tls.certresolver=cf
- traefik.http.routers.wealth.middlewares=pmoves-forward-auth@file,pmoves-auth-redirect@file
- traefik.http.services.wealth.loadbalancer.server.port=8080
volumes:
- firefly-storage:/var/www/html/storage
- ./integrations/pr-kits/firefly/nginx-buffers.conf:/etc/nginx/conf.d/zz_buffers.conf:ro
Expand Down Expand Up @@ -146,9 +172,17 @@ services:
SURREAL_PASS: ${OPEN_NOTEBOOK_SURREAL_PASS:-${SURREAL_PASS:?Set OPEN_NOTEBOOK_SURREAL_PASS or SURREAL_PASS}}
SURREAL_NAMESPACE: ${OPEN_NOTEBOOK_SURREAL_NAMESPACE:-open-notebook}
SURREAL_DATABASE: ${OPEN_NOTEBOOK_SURREAL_DATABASE:-open-notebook}
ports:
- "${OPEN_NOTEBOOK_UI_PORT:-8503}:8502"
- "${OPEN_NOTEBOOK_API_PORT:-5055}:5055"
# No host `ports:` — reachable only through Traefik (notebook.pmoves.ai). The
# UI (8502) is fronted; the API (5055) is internal (UI calls it on localhost).
# RemoteUserMiddleware in the fork (Task 8) trusts the injected Remote-User.
labels:
- traefik.enable=true
- traefik.docker.network=pmoves_external
- traefik.http.routers.notebook.rule=Host(`notebook.pmoves.ai`)
- traefik.http.routers.notebook.entrypoints=websecure
- traefik.http.routers.notebook.tls.certresolver=cf
- traefik.http.routers.notebook.middlewares=pmoves-forward-auth@file,pmoves-auth-redirect@file
- traefik.http.services.notebook.loadbalancer.server.port=8502

jellyfin-ext:
image: ${JELLYFIN_IMAGE:-ghcr.io/powerfulmoves/pmoves-jellyfin:pmoves-latest}
Expand All @@ -165,8 +199,16 @@ services:
environment:
JELLYFIN_PublishedServerUrl: ${JELLYFIN_PUBLISHED_URL:-http://localhost:8096}
JELLYFIN_MEDIA_ROOT: ${JELLYFIN_MEDIA_ROOT:-/media}
ports:
- "8096:8096"
# No host `ports:` — reachable only through Traefik (media.pmoves.ai). NOTE:
# NO forward-auth middleware here — Jellyfin's OIDC plugin (Task 9) handles
# auth itself; the plugin's web UI must reach /oidc/* unauthenticated.
labels:
- traefik.enable=true
- traefik.docker.network=pmoves_external
- traefik.http.routers.media.rule=Host(`media.pmoves.ai`)
- traefik.http.routers.media.entrypoints=websecure
- traefik.http.routers.media.tls.certresolver=cf
- traefik.http.services.media.loadbalancer.server.port=8096
volumes:
- ./data/jellyfin/config:/config
- ./data/jellyfin/cache:/cache
Expand Down
29 changes: 29 additions & 0 deletions pmoves/docker-compose.sso.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
services:
sso-auth:
build: { context: ., dockerfile: services/sso-auth/Dockerfile }
image: ${SSO_AUTH_IMAGE:-pmoves/sso-auth:local}
container_name: pmoves-sso-auth
restart: unless-stopped
environment:
SUPABASE_JWT_SECRET: ${SUPABASE_JWT_SECRET:-${JWT_SECRET}}
GOTRUE_URL: ${GOTRUE_URL:-http://supabase-gotrue:9999}
PUBLIC_BASE_URL: ${SSO_PUBLIC_BASE_URL:-https://auth.pmoves.ai}
SSO_COOKIE_DOMAIN: ${SSO_COOKIE_DOMAIN:-.pmoves.ai}
JELLYFIN_OIDC_CLIENT_ID: ${JELLYFIN_OIDC_CLIENT_ID}
JELLYFIN_OIDC_CLIENT_SECRET: ${JELLYFIN_OIDC_CLIENT_SECRET}
JELLYFIN_OIDC_REDIRECT_URIS: ${JELLYFIN_OIDC_REDIRECT_URIS:-https://media.pmoves.ai/sso/OID/redirect/pmoves}
networks: { pmoves_app: { aliases: [pmoves-sso-auth] }, pmoves_external: {} }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Attach sso-auth to the GoTrue network

With the default GOTRUE_URL=http://supabase-gotrue:9999, password login, OAuth exchange, and logout require container DNS access to supabase-gotrue. The checked docker-compose.yml attaches GoTrue only to pmoves_api, whereas this service joins only pmoves_app and pmoves_external, so the default endpoint cannot resolve or connect; attach the auth service to pmoves_api or use a reachable endpoint.

Useful? React with 👍 / 👎.

security_opt: [no-new-privileges:true]
cap_drop: [ALL]
# Traefik MUST see this container to resolve `sso-auth@docker` — the service
# the auth.pmoves.ai router (defined on the traefik container) points at.
# Under exposedbydefault=false a container is invisible without
# traefik.enable=true, and dual-homed it needs the explicit network. Without
# these the login page never registers and SSO breaks at the front door.
labels:
- traefik.enable=true
- traefik.docker.network=pmoves_external
- traefik.http.services.sso-auth.loadbalancer.server.port=8080
networks:
pmoves_app: { external: true }
pmoves_external: { external: true }
33 changes: 33 additions & 0 deletions pmoves/docker-compose.traefik.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
services:
traefik:
image: ${TRAEFIK_IMAGE:-traefik:v3.3}
container_name: pmoves-traefik
restart: unless-stopped
command:
- --providers.docker=true
- --providers.docker.exposedbydefault=false
- --providers.file.filename=/etc/traefik/dynamic.yml
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
- --entrypoints.web.http.redirections.entrypoint.to=websecure
- --certificatesresolvers.cf.acme.dnschallenge=true
- --certificatesresolvers.cf.acme.dnschallenge.provider=cloudflare
- --certificatesresolvers.cf.acme.email=${ACME_EMAIL:-ops@pmoves.ai}
- --certificatesresolvers.cf.acme.storage=/letsencrypt/acme.json
environment:
CF_DNS_API_TOKEN: ${CLOUDFLARE_DNS_API_TOKEN}
ports: ["443:443", "80:80"]
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./config/traefik/dynamic.yml:/etc/traefik/dynamic.yml:ro
- traefik-acme:/letsencrypt
networks: { pmoves_external: {} }
labels:
- traefik.enable=true
- traefik.http.routers.auth.rule=Host(`auth.pmoves.ai`)
- traefik.http.routers.auth.entrypoints=websecure
- traefik.http.routers.auth.tls.certresolver=cf
- traefik.http.routers.auth.service=sso-auth@docker
volumes: { traefik-acme: {} }
networks:
pmoves_external: { external: true }
Loading
Loading