Add SESSION_COOKIE_DOMAIN so /admin SSR sees the session cookie
Production splits web (descrybe.io) and API (api.descrybe.io). The session cookie was host-only for api.descrybe.io, so the browser never sent it to the web host. The /admin SvelteKit SSR gate (fetchMeStaff in +layout.server.ts) forwards the incoming cookie header to /api/auth/me — with no cookie to forward it always got 401 and bounced every successful login back to /login?next=/admin (login POST 200, /me 200 from the browser, /me 401 from the web server). New SESSION_COOKIE_DOMAIN env (default empty = host-only, local dev unchanged) sets the session cookie Domain attribute; set it to the parent domain (descrybe.io) in production so both hosts receive the cookie. Leading dot is normalized away. CSRF needs no change — it already seeds cross-origin via the X-CSRF-Token response header. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/alexedwards/scs/pgxstore"
|
||||
@@ -9,7 +10,14 @@ import (
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
func NewSessionManager(pool *pgxpool.Pool, cookieName string, secure bool, idleHours int) *scs.SessionManager {
|
||||
// NewSessionManager builds the scs session manager. cookieDomain is the session
|
||||
// cookie Domain attribute: empty keeps a host-only cookie (localhost / same-host
|
||||
// deploys). When the web app and API live on sibling hosts of one parent domain
|
||||
// (descrybe.io + api.descrybe.io), set SESSION_COOKIE_DOMAIN=descrybe.io so the
|
||||
// browser also sends the session cookie to the web host — SvelteKit SSR gates
|
||||
// (/admin +layout.server.ts fetchMeStaff) forward it to /api/auth/me and would
|
||||
// otherwise always see 401.
|
||||
func NewSessionManager(pool *pgxpool.Pool, cookieName, cookieDomain string, secure bool, idleHours int) *scs.SessionManager {
|
||||
sm := scs.New()
|
||||
sm.Store = pgxstore.New(pool)
|
||||
sm.Lifetime = 7 * 24 * time.Hour
|
||||
@@ -18,6 +26,7 @@ func NewSessionManager(pool *pgxpool.Pool, cookieName string, secure bool, idleH
|
||||
}
|
||||
sm.IdleTimeout = time.Duration(idleHours) * time.Hour
|
||||
sm.Cookie.Name = cookieName
|
||||
sm.Cookie.Domain = strings.TrimPrefix(strings.TrimSpace(cookieDomain), ".")
|
||||
sm.Cookie.HttpOnly = true
|
||||
sm.Cookie.Secure = secure
|
||||
sm.Cookie.SameSite = http.SameSiteLaxMode
|
||||
|
||||
@@ -9,10 +9,13 @@ import (
|
||||
func TestNewSessionManagerCookieFlags(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
sm := NewSessionManager(nil, "descrybe_session", true, 12)
|
||||
sm := NewSessionManager(nil, "descrybe_session", "", true, 12)
|
||||
if sm.Cookie.Name != "descrybe_session" {
|
||||
t.Fatalf("Name = %q", sm.Cookie.Name)
|
||||
}
|
||||
if sm.Cookie.Domain != "" {
|
||||
t.Fatalf("Domain = %q, want host-only default", sm.Cookie.Domain)
|
||||
}
|
||||
if !sm.Cookie.HttpOnly {
|
||||
t.Fatal("session cookie must be HttpOnly")
|
||||
}
|
||||
@@ -32,7 +35,7 @@ func TestNewSessionManagerCookieFlags(t *testing.T) {
|
||||
t.Fatalf("Lifetime = %v, want 7d", sm.Lifetime)
|
||||
}
|
||||
|
||||
insecure := NewSessionManager(nil, "descrybe_session", false, 0)
|
||||
insecure := NewSessionManager(nil, "descrybe_session", "", false, 0)
|
||||
if insecure.Cookie.Secure {
|
||||
t.Fatal("secure=false must not set Secure")
|
||||
}
|
||||
@@ -42,4 +45,11 @@ func TestNewSessionManagerCookieFlags(t *testing.T) {
|
||||
if insecure.IdleTimeout != 24*time.Hour {
|
||||
t.Fatalf("default IdleTimeout = %v, want 24h", insecure.IdleTimeout)
|
||||
}
|
||||
|
||||
// Parent-domain deploys (descrybe.io + api.descrybe.io): leading dot is
|
||||
// normalized away; browsers include subdomains whenever Domain is set.
|
||||
scoped := NewSessionManager(nil, "descrybe_session", ".descrybe.io", true, 12)
|
||||
if scoped.Cookie.Domain != "descrybe.io" {
|
||||
t.Fatalf("Domain = %q, want descrybe.io", scoped.Cookie.Domain)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user