MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m32s
WebSocket and EventSource put the 30-day session token into the URL, where proxies log it. The app now gets a one-time ticket right before connecting (POST /auth/stream-ticket, 60 s, bound to the token version); ?token= is no longer accepted for streams, non-browser clients may use the Authorization header. With BASE_URL set, CORS allows only that origin and WebSocket upgrades only it or the requested host; credentials are off (no cookies exist). Logout, ending all sessions, a new password and deactivation close all live streams of the account; clients with a valid session reconnect with a fresh ticket. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
74 lines
2.1 KiB
Go
74 lines
2.1 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/sandrodach/mindsift/internal/config"
|
|
)
|
|
|
|
func TestStreamTicketsReplaceTokensInURLs(t *testing.T) {
|
|
srv, store, token := setupTestServer(t)
|
|
router := srv.Router()
|
|
ticket := func(tok string) string {
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/stream-ticket", nil)
|
|
req.Header.Set("Authorization", "Bearer "+tok)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
var out struct{ Ticket string }
|
|
_ = json.Unmarshal(rec.Body.Bytes(), &out)
|
|
return out.Ticket
|
|
}
|
|
stream := func(query string) string {
|
|
return srv.authenticateUserFromReq(httptest.NewRequest(http.MethodGet, "/api/v1/events?"+query, nil))
|
|
}
|
|
|
|
tk := ticket(token)
|
|
if tk == "" {
|
|
t.Fatal("no ticket issued")
|
|
}
|
|
if stream("ticket="+tk) == "" {
|
|
t.Fatal("a fresh ticket must open the stream")
|
|
}
|
|
if stream("ticket="+tk) != "" {
|
|
t.Fatal("a ticket works only once")
|
|
}
|
|
if stream("token="+token) != "" {
|
|
t.Fatal("session tokens in the URL are no longer accepted")
|
|
}
|
|
|
|
// Ending all sessions makes unused tickets worthless as well
|
|
tk = ticket(token)
|
|
uid := stream("ticket=" + ticket(token))
|
|
_ = store.RevokeAllUserTokens(uid)
|
|
if stream("ticket="+tk) != "" {
|
|
t.Fatal("ticket from before 'all sessions ended' must be refused")
|
|
}
|
|
}
|
|
|
|
func TestStreamOriginCheck(t *testing.T) {
|
|
srv := &Server{cfg: &config.Config{BaseURL: "https://notes.example.com", BaseURLSet: true}}
|
|
check := func(host, origin string) bool {
|
|
r := httptest.NewRequest(http.MethodGet, "/api/v1/ws", nil)
|
|
r.Host = host
|
|
if origin != "" {
|
|
r.Header.Set("Origin", origin)
|
|
}
|
|
return srv.streamOriginAllowed(r)
|
|
}
|
|
if !check("mindsift:8080", "https://notes.example.com") {
|
|
t.Error("BASE_URL origin must pass even if the proxy rewrote Host")
|
|
}
|
|
if !check("notes.example.com", "https://notes.example.com") {
|
|
t.Error("same host must pass")
|
|
}
|
|
if check("notes.example.com", "https://evil.example.org") {
|
|
t.Error("foreign origin must be refused")
|
|
}
|
|
if !check("notes.example.com", "") {
|
|
t.Error("non-browser clients without Origin pass (they still need a ticket)")
|
|
}
|
|
}
|