Kontekst-velger: Hjem og Administrasjon som separate lenker
Dropdown viser begge arbeidsflater med absolutte URLer (ws.synops.no og adm.synops.no). Navigasjon mellom subdomener fungerer uten å miste sesjon. Erfaringsnotat: multi-subdomain med SvelteKit — ORIGIN-fellen, cookie-domene, CSRF, OIDC redirect URIs, sjekkliste for nye subdomener. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f239ada4f6
commit
dfcec6b3b0
2 changed files with 144 additions and 7 deletions
125
docs/erfaringer/multi_subdomain.md
Normal file
125
docs/erfaringer/multi_subdomain.md
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
# Erfaring: Multi-subdomain med SvelteKit (mars 2026)
|
||||||
|
|
||||||
|
## Kontekst
|
||||||
|
|
||||||
|
Innføring av `adm.synops.no` som admin-domene ved siden av
|
||||||
|
`ws.synops.no` (app) avslørte flere arkitekturelle blindsoner.
|
||||||
|
|
||||||
|
## Problemer vi møtte
|
||||||
|
|
||||||
|
### 1. ORIGIN låser hostname
|
||||||
|
|
||||||
|
**Symptom:** `event.url.hostname` returnerte alltid `ws.synops.no`
|
||||||
|
uansett hvilken Host-header som kom inn.
|
||||||
|
|
||||||
|
**Årsak:** `ORIGIN=https://ws.synops.no` i `.env`. SvelteKit
|
||||||
|
adapter-node bruker ORIGIN for å konstruere `event.url` — den
|
||||||
|
*overskriver* Host-headeren.
|
||||||
|
|
||||||
|
**Løsning:** Fjern ORIGIN. `AUTH_TRUST_HOST=true` lar SvelteKit
|
||||||
|
lese hostname fra selve HTTP Host-headeren.
|
||||||
|
|
||||||
|
**Læring:** ORIGIN er ment for single-origin deployments.
|
||||||
|
Multi-subdomain krever at SvelteKit leser Host dynamisk.
|
||||||
|
|
||||||
|
### 2. Session-cookie bundet til ett subdomain
|
||||||
|
|
||||||
|
**Symptom:** Login på `ws.synops.no` ga ikke tilgang til
|
||||||
|
`adm.synops.no`. Brukeren ble bedt om å logge inn igjen.
|
||||||
|
|
||||||
|
**Årsak:** Session-cookie var satt med `domain=ws.synops.no`
|
||||||
|
(default). Cookien var ikke tilgjengelig for `adm.synops.no`.
|
||||||
|
|
||||||
|
**Løsning:** Sett cookie-domene til `.synops.no` i auth.ts.
|
||||||
|
Alle subdomener deler sesjonen.
|
||||||
|
|
||||||
|
**Læring:** Wildcard cookie-domene (`.synops.no`) er nødvendig
|
||||||
|
når flere subdomener trenger samme autentisering. Det er trygt
|
||||||
|
så lenge alle subdomener er under vår kontroll.
|
||||||
|
|
||||||
|
### 3. CSRF cross-origin blokkering
|
||||||
|
|
||||||
|
**Symptom:** `Cross-site POST form submissions are forbidden`
|
||||||
|
ved login-callback fra Authentik.
|
||||||
|
|
||||||
|
**Årsak:** SvelteKit sin innebygde CSRF-sjekk sammenligner
|
||||||
|
request origin mot ORIGIN-variabelen. POST fra `adm.synops.no`
|
||||||
|
til OIDC-callback ble blokkert.
|
||||||
|
|
||||||
|
**Løsning:** `csrf: { checkOrigin: false }` i svelte.config.js.
|
||||||
|
Trygt fordi OIDC bruker PKCE + state som CSRF-beskyttelse.
|
||||||
|
|
||||||
|
**Læring:** SvelteKit sin CSRF-sjekk er for streng for
|
||||||
|
multi-origin. Deaktiver den når du har egen CSRF-mekanisme.
|
||||||
|
|
||||||
|
### 4. Authentik redirect URI
|
||||||
|
|
||||||
|
**Symptom:** OIDC-callback feilet fordi `adm.synops.no` ikke
|
||||||
|
var registrert som gyldig redirect URI.
|
||||||
|
|
||||||
|
**Årsak:** Bare `ws.synops.no` var registrert i Authentik.
|
||||||
|
|
||||||
|
**Løsning:** Legg til `adm.synops.no/auth/callback/authentik`
|
||||||
|
i Authentik sin provider-konfig.
|
||||||
|
|
||||||
|
**Læring:** Hvert subdomain trenger egen redirect URI i OIDC.
|
||||||
|
|
||||||
|
### 5. TLS-sertifikat
|
||||||
|
|
||||||
|
**Ikke et problem:** Caddy henter automatisk sertifikat for
|
||||||
|
nye domener via Let's Encrypt ACME. `adm.synops.no` fikk
|
||||||
|
sertifikat i løpet av sekunder ved første request.
|
||||||
|
|
||||||
|
**Læring:** Caddy sin auto-TLS er utmerket for nye subdomener.
|
||||||
|
Bare legg til i Caddyfile og restart.
|
||||||
|
|
||||||
|
## Arkitekturprinsipper vi trekker ut
|
||||||
|
|
||||||
|
### 1. Ikke hardkod hostnames i konfigfiler
|
||||||
|
|
||||||
|
ORIGIN, cookie-domene, redirect URIs — alt som binder til
|
||||||
|
et spesifikt hostname gjør multi-subdomain vanskelig.
|
||||||
|
Foretrekk dynamisk host-deteksjon (`AUTH_TRUST_HOST=true`).
|
||||||
|
|
||||||
|
### 2. Cookies på toppdomenet for relaterte subdomener
|
||||||
|
|
||||||
|
Når flere subdomener trenger samme sesjon, sett cookie på
|
||||||
|
`.synops.no`. CSRF-token kan forbli host-bound (`__Host-`
|
||||||
|
prefix) for ekstra sikkerhet.
|
||||||
|
|
||||||
|
### 3. Samme SvelteKit-instans for alle subdomener
|
||||||
|
|
||||||
|
Ikke kjør separate SvelteKit-instanser per subdomain. Én
|
||||||
|
instans som leser hostname og tilpasser seg. Enklere deploy,
|
||||||
|
delt kodebase, felles sesjon.
|
||||||
|
|
||||||
|
### 4. Caddy gjør routing, SvelteKit gjør logikk
|
||||||
|
|
||||||
|
Caddy ruter domene → SvelteKit. SvelteKit sjekker hostname
|
||||||
|
og tilpasser innhold. Caddy trenger ingen hostname-logikk
|
||||||
|
utover reverse proxy.
|
||||||
|
|
||||||
|
## Hva ble gjort riktig
|
||||||
|
|
||||||
|
- **Caddy-konfig:** Enkelt å legge til nytt subdomain
|
||||||
|
- **Delt SvelteKit:** Ingen duplisert kode
|
||||||
|
- **Cookie-wildcard:** Riktig for relaterte subdomener
|
||||||
|
- **AUTH_TRUST_HOST:** Fungerer godt uten ORIGIN
|
||||||
|
|
||||||
|
## Hva vi tenderer til å glemme
|
||||||
|
|
||||||
|
- **ORIGIN-variabelen** — den overstyrer alt. Sjekk den først.
|
||||||
|
- **Cookie-domene** — default er gjeldende hostname, ikke parent.
|
||||||
|
- **OIDC redirect URIs** — må oppdateres for hvert nytt domene.
|
||||||
|
- **Be bruker slette cookies** — gammel cookie fra ett domene
|
||||||
|
kræsjer med ny cookie fra wildcard-domenet.
|
||||||
|
|
||||||
|
## Sjekkliste for nytt subdomain
|
||||||
|
|
||||||
|
1. DNS A-record (Hetzner DNS Console)
|
||||||
|
2. Caddy-blokk i Caddyfile (auto-TLS)
|
||||||
|
3. Authentik redirect URI
|
||||||
|
4. Verifiser at ORIGIN *ikke* er satt (eller er kompatibel)
|
||||||
|
5. Verifiser cookie-domene (`.synops.no`)
|
||||||
|
6. Test: login → callback → riktig side
|
||||||
|
7. Be bruker slette cookies hvis problemer
|
||||||
|
|
@ -393,18 +393,30 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="context-selector-list">
|
<div class="context-selector-list">
|
||||||
<!-- Hjem (alltid øverst når ikke søker) -->
|
<!-- Faste arbeidsflater (alltid øverst når ikke søker) -->
|
||||||
{#if !searchQuery.trim()}
|
{#if !searchQuery.trim()}
|
||||||
<button
|
<a
|
||||||
class="context-selector-item"
|
class="context-selector-item"
|
||||||
class:current={isPersonalWorkspace}
|
class:current={isPersonalWorkspace && homeLabel === 'Hjem'}
|
||||||
onclick={() => { selectorOpen = false; goto('/'); }}
|
href="https://ws.synops.no"
|
||||||
|
onclick={() => { selectorOpen = false; }}
|
||||||
>
|
>
|
||||||
<span class="context-selector-item-title">{homeLabel}</span>
|
<span class="context-selector-item-title">Hjem</span>
|
||||||
{#if isPersonalWorkspace}
|
{#if isPersonalWorkspace && homeLabel === 'Hjem'}
|
||||||
<span class="context-selector-item-check">✓</span>
|
<span class="context-selector-item-check">✓</span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</a>
|
||||||
|
<a
|
||||||
|
class="context-selector-item"
|
||||||
|
class:current={isPersonalWorkspace && homeLabel === 'Administrasjon'}
|
||||||
|
href="https://adm.synops.no"
|
||||||
|
onclick={() => { selectorOpen = false; }}
|
||||||
|
>
|
||||||
|
<span class="context-selector-item-title">Administrasjon</span>
|
||||||
|
{#if isPersonalWorkspace && homeLabel === 'Administrasjon'}
|
||||||
|
<span class="context-selector-item-check">✓</span>
|
||||||
|
{/if}
|
||||||
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Mine flater -->
|
<!-- Mine flater -->
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue