453 lines
20 KiB
Go
453 lines
20 KiB
Go
package web
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"html"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
// R-983 (security review 2026-10-12, six remaining items) and R-976 (family and gate sessions), v0.324.0. Each test
|
|
// drives the handler the browser or traefik reaches. Red-proofs are recorded in the REPORT of the release.
|
|
|
|
// ── R-983 item 1: after sign-in the browser stays on the box ─────────────────────────────────────────────────
|
|
|
|
func TestR983_LoginNextStaysOnTheBox(t *testing.T) {
|
|
s := rateLimitTestServer(t)
|
|
login := func(next string) string {
|
|
form := url.Values{"password": {"correct-pass"}, "next": {next}}
|
|
r := httptest.NewRequest(http.MethodPost, "/login", strings.NewReader(form.Encode()))
|
|
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
r.RemoteAddr = "203.0.113.7:4000"
|
|
w := httptest.NewRecorder()
|
|
s.handleLogin(w, r)
|
|
if w.Code != http.StatusFound {
|
|
t.Fatalf("next=%q: sign-in answered %d, want 302", next, w.Code)
|
|
}
|
|
return w.Header().Get("Location")
|
|
}
|
|
for _, evil := range []string{"//evil.example/x", "/\\evil.example", "https://evil.example/", "/\\/evil.example"} {
|
|
if got := login(evil); got != "/" {
|
|
t.Errorf("next=%q sent the signed-in browser to %q — want /", evil, got)
|
|
}
|
|
}
|
|
if got := login("/stacks?tab=1"); got != "/stacks?tab=1" {
|
|
t.Errorf("a page on the box must still be the destination: got %q", got)
|
|
}
|
|
}
|
|
|
|
// ── R-983 item 2: no page of the dashboard may be framed ─────────────────────────────────────────────────────
|
|
|
|
func TestR983_DashboardRefusesFraming(t *testing.T) {
|
|
s, _ := familyHarness(t)
|
|
reached := false
|
|
h := s.CatchAllMiddleware(s.RequireAuth(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
reached = true
|
|
w.WriteHeader(http.StatusOK)
|
|
})))
|
|
sess := &http.Cookie{Name: sessionCookieName, Value: newTestSession(s)}
|
|
for _, p := range []string{"/launcher", "/login", familyLoginPath, "/settings/security"} {
|
|
r := httptest.NewRequest(http.MethodGet, "https://felhom.example.hu"+p, nil)
|
|
r.AddCookie(sess)
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, r)
|
|
if got := w.Header().Get("X-Frame-Options"); got != "DENY" {
|
|
t.Errorf("%s: X-Frame-Options %q, want DENY", p, got)
|
|
}
|
|
if got := w.Header().Get("Content-Security-Policy"); got != "frame-ancestors 'none'" {
|
|
t.Errorf("%s: Content-Security-Policy %q, want frame-ancestors 'none'", p, got)
|
|
}
|
|
}
|
|
if !reached {
|
|
t.Fatal("the signed-in page was never reached — the test proved nothing about a real page")
|
|
}
|
|
// traefik's forwardAuth answers stay as they were: their headers must not ride onto an app's responses.
|
|
r := httptest.NewRequest(http.MethodGet, "http://felhom-controller:8080"+familyAuthPath, nil)
|
|
r.Header.Set("X-Forwarded-Host", "fapp.example.hu")
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, r)
|
|
if w.Header().Get("X-Frame-Options") != "" || w.Header().Get("Content-Security-Policy") != "" {
|
|
t.Error("the forwardAuth answer must carry no framing header")
|
|
}
|
|
}
|
|
|
|
// ── R-983 item 3: parallel sign-in tries are counted ─────────────────────────────────────────────────────────
|
|
|
|
// The compare is a seam that waits until every try has arrived (or a short deadline): with check-then-count all 20
|
|
// tries reach it; with the try reserved first only loginMaxAttempts do.
|
|
func TestR983_ParallelLoginsHitTheLimit(t *testing.T) {
|
|
s := rateLimitTestServer(t)
|
|
const tries = 20
|
|
var calls int32
|
|
all := make(chan struct{})
|
|
var once sync.Once
|
|
s.passwordCompare = func(hash, pw []byte) error {
|
|
if atomic.AddInt32(&calls, 1) == tries {
|
|
once.Do(func() { close(all) })
|
|
}
|
|
select {
|
|
case <-all:
|
|
case <-time.After(400 * time.Millisecond):
|
|
}
|
|
return errors.New("mismatch")
|
|
}
|
|
var wg sync.WaitGroup
|
|
start := make(chan struct{})
|
|
for i := 0; i < tries; i++ {
|
|
wg.Add(1)
|
|
go func(i int) {
|
|
defer wg.Done()
|
|
<-start
|
|
doLogin(s, fmt.Sprintf("198.51.100.9:%d", 6000+i), "", "wrong")
|
|
}(i)
|
|
}
|
|
close(start)
|
|
wg.Wait()
|
|
if got := atomic.LoadInt32(&calls); got > loginMaxAttempts {
|
|
t.Fatalf("%d parallel wrong tries reached the password compare — the limit is %d per visitor", got, loginMaxAttempts)
|
|
}
|
|
// and the household's right password is still refused only for this visitor, not for everyone
|
|
s.passwordCompare = nil
|
|
if body := doLogin(s, "203.0.113.50:7000", "", "correct-pass"); body.Code != http.StatusFound {
|
|
t.Fatalf("another visitor with the right password must get in: %d", body.Code)
|
|
}
|
|
}
|
|
|
|
func TestR983_ParallelFamilySignInsHitTheLimit(t *testing.T) {
|
|
s, st := familyHarness(t)
|
|
st.SetCost(10) // a real compare takes tens of milliseconds, so the tries overlap
|
|
if _, err := st.Add("anna"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
const tries = 20
|
|
var wrong, locked int32
|
|
var wg sync.WaitGroup
|
|
start := make(chan struct{})
|
|
for i := 0; i < tries; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
<-start
|
|
switch famLogin(s, "198.51.100.66", "anna", "wrong", "").Code {
|
|
case http.StatusUnauthorized:
|
|
atomic.AddInt32(&wrong, 1)
|
|
case http.StatusTooManyRequests:
|
|
atomic.AddInt32(&locked, 1)
|
|
}
|
|
}()
|
|
}
|
|
close(start)
|
|
wg.Wait()
|
|
if wrong > familyVisitorMax {
|
|
t.Fatalf("%d parallel tries had their password checked — the limit is %d per visitor (locked: %d)", wrong, familyVisitorMax, locked)
|
|
}
|
|
if wrong+locked != tries {
|
|
t.Fatalf("every try must be answered as wrong or locked: %d + %d of %d", wrong, locked, tries)
|
|
}
|
|
}
|
|
|
|
// ── R-983 item 4: the settings page's minimum is the claim's ─────────────────────────────────────────────────
|
|
|
|
func TestR983_SettingsPasswordMinimumIsTheClaims(t *testing.T) {
|
|
if claimMinPassword != 12 {
|
|
t.Fatalf("the claim's minimum moved to %d — this test's literals assume 12", claimMinPassword)
|
|
}
|
|
s, _ := familyHarness(t)
|
|
tok := newTestSession(s)
|
|
change := func(pw string) *httptest.ResponseRecorder {
|
|
form := url.Values{"current_password": {"dashboard-pass"}, "new_password": {pw}, "confirm_password": {pw}}
|
|
r := httptest.NewRequest(http.MethodPost, "https://felhom.example.hu/settings/password", strings.NewReader(form.Encode()))
|
|
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
r.AddCookie(&http.Cookie{Name: sessionCookieName, Value: tok})
|
|
w := httptest.NewRecorder()
|
|
s.settingsPasswordHandler(w, r)
|
|
return w
|
|
}
|
|
w := change("eleven-char")
|
|
if w.Code == http.StatusFound {
|
|
t.Fatal("an 11-character password was accepted — the claim requires 12")
|
|
}
|
|
if !strings.Contains(w.Body.String(), "legalább 12 karakter") {
|
|
t.Errorf("the refusal must name the claim's minimum (Hungarian)")
|
|
}
|
|
if bcrypt.CompareHashAndPassword([]byte(s.effectivePasswordHash()), []byte("dashboard-pass")) != nil {
|
|
t.Fatal("the refused change altered the password")
|
|
}
|
|
// the page's own form says the same minimum (both languages render it from one value)
|
|
page := w.Body.String()
|
|
if !strings.Contains(page, `minlength="12"`) || strings.Contains(page, `minlength="8"`) {
|
|
t.Errorf("the password form must ask for at least 12 characters")
|
|
}
|
|
if got := s.msgLang("en", "settings.pw_too_short", claimMinPassword); !strings.Contains(got, "at least 12 characters") {
|
|
t.Errorf("English refusal %q must name 12", got)
|
|
}
|
|
if w := change("twelve-chars"); w.Code != http.StatusFound {
|
|
t.Fatalf("a 12-character password must be accepted: %d", w.Code)
|
|
}
|
|
}
|
|
|
|
// ── R-983 item 5: reset-code requests are limited on the box ─────────────────────────────────────────────────
|
|
|
|
func TestR983_ResetCodeRequestsAreLimited(t *testing.T) {
|
|
s, _, _ := claimTestServer(t)
|
|
now := time.Date(2026, 10, 16, 2, 0, 0, 0, time.UTC)
|
|
s.claimClock = func() time.Time { return now }
|
|
var calls int
|
|
s.resetRequestFn = func() { calls++ }
|
|
ask := func(visitor string) {
|
|
form := url.Values{"_csrf": {s.claimCSRFToken()}}
|
|
r := httptest.NewRequest(http.MethodPost, "/claim/request-new-code", strings.NewReader(form.Encode()))
|
|
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
r.AddCookie(&http.Cookie{Name: claimCSRFCookie, Value: s.claimCSRFToken()})
|
|
r.RemoteAddr = visitor + ":5000"
|
|
w := httptest.NewRecorder()
|
|
s.handleClaimRequestNewCode(w, r)
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("the answer must stay the neutral page: %d", w.Code)
|
|
}
|
|
}
|
|
for i := 0; i < 3; i++ {
|
|
ask("198.51.100.1")
|
|
}
|
|
if calls != 1 {
|
|
t.Fatalf("3 requests in one minute from one visitor reached the hub %d times — want 1", calls)
|
|
}
|
|
now = now.Add(resetReqVisitorEvery + time.Second)
|
|
ask("198.51.100.1")
|
|
if calls != 2 {
|
|
t.Fatalf("after a minute the same visitor must reach the hub again: %d calls", calls)
|
|
}
|
|
ask("198.51.100.2")
|
|
ask("198.51.100.3") // the box's budget for the quarter hour is used here
|
|
if calls != resetReqGlobalMax {
|
|
t.Fatalf("calls %d, want %d", calls, resetReqGlobalMax)
|
|
}
|
|
ask("198.51.100.4")
|
|
if calls != resetReqGlobalMax {
|
|
t.Fatalf("a fourth visitor in the same quarter hour reached the hub: %d calls", calls)
|
|
}
|
|
now = now.Add(resetReqGlobalWindow)
|
|
ask("198.51.100.4")
|
|
if calls != resetReqGlobalMax+1 {
|
|
t.Fatalf("after the window a request must reach the hub again (the household is not locked out for long): %d", calls)
|
|
}
|
|
}
|
|
|
|
// ── R-983 item 6: logout cannot be triggered from another page ───────────────────────────────────────────────
|
|
|
|
func TestR983_LogoutNeedsCSRF(t *testing.T) {
|
|
s, _ := familyHarness(t)
|
|
tok := newTestSession(s)
|
|
h := s.RequireAuth(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(299) }))
|
|
out := func(method, csrf string) *httptest.ResponseRecorder {
|
|
var body *strings.Reader
|
|
if csrf != "" {
|
|
body = strings.NewReader(url.Values{csrfFormField: {csrf}}.Encode())
|
|
} else {
|
|
body = strings.NewReader("")
|
|
}
|
|
r := httptest.NewRequest(method, "https://felhom.example.hu/logout", body)
|
|
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
r.AddCookie(&http.Cookie{Name: sessionCookieName, Value: tok})
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, r)
|
|
return w
|
|
}
|
|
if w := out(http.MethodPost, ""); w.Code != http.StatusForbidden || !s.isValidSession(tok) {
|
|
t.Fatalf("a POST without the token: %d, session valid %v — want 403 and the session kept", w.Code, s.isValidSession(tok))
|
|
}
|
|
if w := out(http.MethodPost, "not-the-token"); w.Code != http.StatusForbidden || !s.isValidSession(tok) {
|
|
t.Fatalf("a POST with a wrong token: %d — want 403 and the session kept", w.Code)
|
|
}
|
|
if w := out(http.MethodGet, ""); w.Code != http.StatusFound || w.Header().Get("Location") != "/" || !s.isValidSession(tok) {
|
|
t.Fatalf("a GET must change nothing: %d %q", w.Code, w.Header().Get("Location"))
|
|
}
|
|
// The dashboard's own control: a POST form carrying the session's token.
|
|
r := httptest.NewRequest(http.MethodGet, "https://felhom.example.hu/settings/security", nil)
|
|
r.AddCookie(&http.Cookie{Name: sessionCookieName, Value: tok})
|
|
pw := httptest.NewRecorder()
|
|
s.settingsSecurityPageHandler(pw, r)
|
|
m := regexp.MustCompile(`<form method="post" action="/logout" class="logout-form"><input type="hidden" name="_csrf" value="([0-9a-f]+)">`).FindStringSubmatch(pw.Body.String())
|
|
if m == nil {
|
|
t.Fatal("the dashboard's sign-out control must be a POST form carrying the session's token")
|
|
}
|
|
if w := out(http.MethodPost, m[1]); w.Code != http.StatusFound || w.Header().Get("Location") != "/login" || s.isValidSession(tok) {
|
|
t.Fatalf("the dashboard's own sign-out: %d %q, session valid %v — want 302 /login and the session ended", w.Code, w.Header().Get("Location"), s.isValidSession(tok))
|
|
}
|
|
}
|
|
|
|
func TestR983_FamilyLogoutNeedsItsToken(t *testing.T) {
|
|
s, st := familyHarness(t)
|
|
pw, _ := st.Add("anna")
|
|
sess := famCookie(famLogin(s, "203.0.113.10", "anna", pw, ""), familySessionCookie)
|
|
app := famPass(t, s, sess)
|
|
ok := func() bool {
|
|
return famAsk(s, "fapp.example.hu", "GET", "/", "application/json", app).Code == http.StatusOK
|
|
}
|
|
out := func(method, ft string) *httptest.ResponseRecorder {
|
|
r := httptest.NewRequest(method, "https://felhom.example.hu"+familyLogoutPath, strings.NewReader(url.Values{"_ft": {ft}}.Encode()))
|
|
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
r.AddCookie(sess)
|
|
w := httptest.NewRecorder()
|
|
s.ServeFamilyLogout(w, r)
|
|
return w
|
|
}
|
|
out(http.MethodGet, "")
|
|
out(http.MethodPost, "")
|
|
out(http.MethodPost, s.familyFormToken()) // the sign-in form's token: anyone can fetch one
|
|
if !ok() {
|
|
t.Fatal("a GET, a tokenless POST or a POST with the sign-in form's token ended the family session")
|
|
}
|
|
// the page's own sign-out form carries the session's token, and that ends it
|
|
r := httptest.NewRequest(http.MethodGet, "https://felhom.example.hu"+familyLoginPath, nil)
|
|
r.AddCookie(sess)
|
|
pg := httptest.NewRecorder()
|
|
s.ServeFamilyLogin(pg, r)
|
|
m := regexp.MustCompile(`action="/__family/logout">\s*<input type="hidden" name="_ft" value="([^"]+)">`).FindStringSubmatch(pg.Body.String())
|
|
if m == nil {
|
|
t.Fatal("the family page's sign-out form must carry its token")
|
|
}
|
|
if w := out(http.MethodPost, html.UnescapeString(m[1])); w.Code != http.StatusOK {
|
|
t.Fatalf("the page's own sign-out: %d", w.Code)
|
|
}
|
|
if ok() {
|
|
t.Fatal("the family page's own sign-out must end the session")
|
|
}
|
|
}
|
|
|
|
// ── R-976 (b): a family app cookie cannot be lifted into another app ─────────────────────────────────────────
|
|
|
|
func TestR976_AnAppCookieCannotBeLiftedIntoAnotherApp(t *testing.T) {
|
|
s, st := familyHarness(t)
|
|
pw, _ := st.Add("anna")
|
|
sess := famCookie(famLogin(s, "203.0.113.10", "anna", pw, ""), familySessionCookie)
|
|
app := famPass(t, s, sess)
|
|
if strings.Contains(app.Value, sess.Value) {
|
|
t.Fatal("the app cookie carries the family session itself — an app's backend could lift it")
|
|
}
|
|
// what an app backend sees, set as the family session on the dashboard host, must mint nothing
|
|
lifted := strings.SplitN(app.Value, ".", 2)[0]
|
|
rd := "https://fapp.example.hu/"
|
|
r := httptest.NewRequest(http.MethodGet, "https://felhom.example.hu"+familyStartPath+"?"+url.Values{"rd": {rd}}.Encode(), nil)
|
|
r.AddCookie(&http.Cookie{Name: familySessionCookie, Value: lifted})
|
|
w := httptest.NewRecorder()
|
|
s.ServeFamilyStart(w, r)
|
|
if loc := w.Header().Get("Location"); strings.Contains(loc, familyCallbackURI) {
|
|
t.Fatalf("a value lifted from an app cookie minted a pass: %q", loc)
|
|
}
|
|
// the sign-in token in flight carries no session either
|
|
tok := s.mintFamilyToken("fapp.example.hu", sess.Value, rd)
|
|
if dec, _ := base64.RawURLEncoding.DecodeString(tok); strings.Contains(string(dec), sess.Value) {
|
|
t.Fatal("the sign-in token carries the family session id")
|
|
}
|
|
// a cookie of the old shape (the session id itself, correctly signed) is refused
|
|
exp := strconv.FormatInt(time.Now().Add(time.Hour).Unix(), 10)
|
|
old := &http.Cookie{Name: familyAppCookie, Value: sess.Value + "." + exp + "." + s.gateMAC("famcookie", "fapp.example.hu", sess.Value, exp)}
|
|
if w := famAsk(s, "fapp.example.hu", "GET", "/", "application/json", old); w.Code == http.StatusOK {
|
|
t.Fatal("an app cookie carrying the session id must not pass any more")
|
|
}
|
|
// the genuine cookie still passes its own app
|
|
if w := famAsk(s, "fapp.example.hu", "GET", "/", "application/json", app); w.Code != http.StatusOK {
|
|
t.Fatalf("the member's own app cookie must pass: %d", w.Code)
|
|
}
|
|
}
|
|
|
|
// ── R-976 (a): a password change or reset ends every family and gate session ─────────────────────────────────
|
|
|
|
// r976World signs anna in (family session + app cookie) and walks the household through the setup gate (gate pass).
|
|
func r976World(t *testing.T, s *Server) (famSess, famApp, gatePass *http.Cookie) {
|
|
t.Helper()
|
|
pw, err := s.familyStore().Add("anna")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
famSess = famCookie(famLogin(s, "203.0.113.10", "anna", pw, ""), familySessionCookie)
|
|
famApp = famPass(t, s, famSess)
|
|
dash := &http.Cookie{Name: sessionCookieName, Value: newTestSession(s)}
|
|
w := gateStart(s, "https://gapp.example.hu/", dash)
|
|
cb, _ := url.Parse(w.Header().Get("Location"))
|
|
w = gateAsk(s, "gapp.example.hu", "GET", cb.RequestURI(), "text/html")
|
|
for _, c := range w.Result().Cookies() {
|
|
if c.Name == gateCookieName {
|
|
gatePass = c
|
|
}
|
|
}
|
|
if gatePass == nil {
|
|
t.Fatal("setup: no gate pass")
|
|
}
|
|
r976AllOpen(t, s, famSess, famApp, gatePass, true)
|
|
return famSess, famApp, gatePass
|
|
}
|
|
|
|
func r976AllOpen(t *testing.T, s *Server, famSess, famApp, gatePass *http.Cookie, want bool) {
|
|
t.Helper()
|
|
appOK := famAsk(s, "fapp.example.hu", "GET", "/", "application/json", famApp).Code == http.StatusOK
|
|
gateOK := gateAsk(s, "gapp.example.hu", "GET", "/", "application/json", gatePass).Code == http.StatusOK
|
|
r := httptest.NewRequest(http.MethodGet, "https://felhom.example.hu"+familyStartPath+"?"+url.Values{"rd": {"https://fapp.example.hu/"}}.Encode(), nil)
|
|
r.AddCookie(famSess)
|
|
w := httptest.NewRecorder()
|
|
s.ServeFamilyStart(w, r)
|
|
sessOK := strings.Contains(w.Header().Get("Location"), familyCallbackURI)
|
|
if appOK != want || gateOK != want || sessOK != want {
|
|
t.Fatalf("family app cookie passes %v, setup gate pass passes %v, family session mints %v — want all %v", appOK, gateOK, sessOK, want)
|
|
}
|
|
}
|
|
|
|
func TestR976_PasswordChangeEndsFamilyAndGateSessions(t *testing.T) {
|
|
s, st := familyHarness(t)
|
|
famSess, famApp, gatePass := r976World(t, s)
|
|
form := url.Values{"current_password": {"dashboard-pass"}, "new_password": {"a-new-password-12"}, "confirm_password": {"a-new-password-12"}}
|
|
r := httptest.NewRequest(http.MethodPost, "https://felhom.example.hu/settings/password", strings.NewReader(form.Encode()))
|
|
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
w := httptest.NewRecorder()
|
|
s.settingsPasswordHandler(w, r)
|
|
if w.Code != http.StatusFound {
|
|
t.Fatalf("the password change: %d", w.Code)
|
|
}
|
|
r976AllOpen(t, s, famSess, famApp, gatePass, false)
|
|
// the members stay; anna signs in again with her own password
|
|
if names := st.Names(); len(names) != 1 || names[0] != "anna" {
|
|
t.Fatalf("the family list must be kept: %v", names)
|
|
}
|
|
}
|
|
|
|
func TestR976_ClaimResetEndsFamilyAndGateSessions(t *testing.T) {
|
|
s, _ := familyHarness(t)
|
|
s.cfg.Web.SessionSecret = "test-session-secret-abcdef"
|
|
code := "alma-korte-szilva"
|
|
hash, _ := bcrypt.GenerateFromPassword([]byte(code), bcrypt.MinCost)
|
|
if err := s.settings.SetClaimCode(string(hash), 1, time.Now().UTC().Format(time.RFC3339)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
famSess, famApp, gatePass := r976World(t, s)
|
|
form := url.Values{"_csrf": {s.claimCSRFToken()}, "code": {code}, "new_password": {"a-new-password-12"}, "confirm_password": {"a-new-password-12"}}
|
|
r := httptest.NewRequest(http.MethodPost, "/claim", strings.NewReader(form.Encode()))
|
|
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
r.AddCookie(&http.Cookie{Name: claimCSRFCookie, Value: s.claimCSRFToken()})
|
|
w := httptest.NewRecorder()
|
|
s.handleClaimSubmit(w, r)
|
|
if w.Code != http.StatusFound || w.Header().Get("Location") != "/" {
|
|
t.Fatalf("the reset: %d %q", w.Code, w.Header().Get("Location"))
|
|
}
|
|
r976AllOpen(t, s, famSess, famApp, gatePass, false)
|
|
// a restart must not bring the old passes back: the rotated key is what is on disk
|
|
s2 := &Server{cfg: s.cfg, settings: s.settings, stackMgr: s.stackMgr, logger: s.logger, version: "test",
|
|
sessions: map[string]*session{}, familyStoreOverride: s.familyStoreOverride}
|
|
s2.loadTemplates()
|
|
if w := gateAsk(s2, "gapp.example.hu", "GET", "/", "application/json", gatePass); w.Code == http.StatusOK {
|
|
t.Fatal("after a restart the old gate pass opened the app again")
|
|
}
|
|
}
|