v0.333.0: the file manager's own token ends with the dashboard sign-out (R-1155); the recovery-code page says the code becomes live after the check (R-1161)
gates / gates (push) Successful in 49s
gates / gates (push) Successful in 49s
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
@@ -381,3 +383,68 @@ func TestFilesSSO_R1100_CrossSiteNoPass(t *testing.T) {
|
||||
t.Fatal("a name with two dots inside it is a real folder name")
|
||||
}
|
||||
}
|
||||
|
||||
// fbToken is a FileBrowser 1.5.6 token's shape (claims measured on demo-hp 2026-10-19: belongsTo, Permissions{…}, iat,
|
||||
// exp, iss; HS256). The signature is junk: the gate never checks it, FileBrowser does.
|
||||
func fbToken(admin bool, id int) string {
|
||||
enc := func(v any) string { b, _ := json.Marshal(v); return base64.RawURLEncoding.EncodeToString(b) }
|
||||
return enc(map[string]any{"alg": "HS256", "typ": "JWT"}) + "." + enc(map[string]any{
|
||||
"belongsTo": id, "iat": 1, "exp": 4102444800, "iss": "FileBrowser Quantum",
|
||||
"Permissions": map[string]bool{"admin": admin, "api": admin, "modify": true, "share": true, "realtime": true, "delete": true, "create": true, "download": true},
|
||||
}) + ".c2lnbmF0dXJl"
|
||||
}
|
||||
|
||||
// R-1155: FileBrowser's own 2-hour token for the household user must not outlive the dashboard session. A browser that
|
||||
// lost only the files cookie (or a token copied out of it) is refused like a stale files cookie — by cookie, by Bearer
|
||||
// header and by the `auth` query parameter; `admin`'s token and FileBrowser's own sign-in page still pass.
|
||||
func TestFilesSSO_R1155_HouseholdTokenDiesWithTheSignOut(t *testing.T) {
|
||||
s := newFilesServer(t)
|
||||
sess := sessionCookie(s)
|
||||
fc := signIn(t, s, sess, "/")
|
||||
header := s.filesHeader()
|
||||
household := &http.Cookie{Name: fileBrowserJWTCookie, Value: fbToken(false, 3)}
|
||||
admin := &http.Cookie{Name: fileBrowserJWTCookie, Value: fbToken(true, 1)}
|
||||
|
||||
if w := filesAsk(s, "files.example.hu", "/files/", htmlAccept, fc, household); w.Code != http.StatusOK || w.Header().Get(header) != infra.FileBrowserSSOUser {
|
||||
t.Fatalf("signed in, files cookie + the household's token: %d header=%q, want 200 with the household", w.Code, w.Header().Get(header))
|
||||
}
|
||||
|
||||
// The household signs out on the dashboard; the browser kept FileBrowser's token but not the files cookie.
|
||||
s.sessionsMu.Lock()
|
||||
delete(s.sessions, sessionFingerprint(sess.Value))
|
||||
s.sessionsMu.Unlock()
|
||||
w := filesAsk(s, "files.example.hu", "/files/", htmlAccept, household)
|
||||
if w.Code == http.StatusOK {
|
||||
t.Fatalf("after the sign-out the household's FileBrowser token alone still passed (200) — it outlives the session")
|
||||
}
|
||||
if loc := w.Header().Get("Location"); w.Code != http.StatusFound || !strings.HasPrefix(loc, "https://felhom.example.hu"+filesOpenPath) {
|
||||
t.Fatalf("browser with the token alone: %d → %q, want 302 to the dashboard", w.Code, loc)
|
||||
}
|
||||
if c := filesCookieIn(w, fileBrowserJWTCookie); c == nil || c.MaxAge >= 0 {
|
||||
t.Fatal("the household's FileBrowser token was not cleared")
|
||||
}
|
||||
if api := filesAsk(s, "files.example.hu", "/api/users?id=self", "application/json", household); api.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("API call with the household's token cookie alone: %d, want 401", api.Code)
|
||||
}
|
||||
r := httptest.NewRequest(http.MethodGet, "http://felhom-controller:8080"+filesAuthPath, nil)
|
||||
r.Header.Set("X-Forwarded-Host", "files.example.hu")
|
||||
r.Header.Set("X-Forwarded-Uri", "/api/users?id=self")
|
||||
r.Header.Set("Authorization", "Bearer "+household.Value)
|
||||
if b := s.filesEntry(r); b.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("API call with the household's token as Bearer: %d, want 401", b.Code)
|
||||
}
|
||||
if q := filesAsk(s, "files.example.hu", "/api/raw?files=x&auth="+url.QueryEscape(household.Value), "*/*"); q.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("download with the household's token in ?auth=: %d, want 401", q.Code)
|
||||
}
|
||||
|
||||
// The fallback stays: admin's token, FileBrowser's own sign-in page, and a token FileBrowser itself will refuse.
|
||||
if a := filesAsk(s, "files.example.hu", "/files/", htmlAccept, admin); a.Code != http.StatusOK || a.Header().Get(header) != "" {
|
||||
t.Fatalf("admin's own token: %d header=%q, want 200 without the household", a.Code, a.Header().Get(header))
|
||||
}
|
||||
if p := filesAsk(s, "files.example.hu", "/login", htmlAccept); p.Code != http.StatusOK {
|
||||
t.Fatalf("FileBrowser's sign-in page with no token: %d, want 200", p.Code)
|
||||
}
|
||||
if g := filesAsk(s, "files.example.hu", "/files/", htmlAccept, &http.Cookie{Name: fileBrowserJWTCookie, Value: "not-a-token"}); g.Code != http.StatusOK {
|
||||
t.Fatalf("an unreadable token: %d, want 200 (FileBrowser refuses it itself)", g.Code)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user