85 lines
3.2 KiB
Go
85 lines
3.2 KiB
Go
package onboarding
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"bytes"
|
||
|
|
"encoding/json"
|
||
|
|
"net/http"
|
||
|
|
"net/http/httptest"
|
||
|
|
"testing"
|
||
|
|
|
||
|
|
"data-terminal-cloud/internal/identity"
|
||
|
|
"github.com/google/uuid"
|
||
|
|
)
|
||
|
|
|
||
|
|
const webBLECSRF = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
||
|
|
|
||
|
|
func webBLESession(t *testing.T, f *fixture, p identity.Principal) string {
|
||
|
|
t.Helper()
|
||
|
|
session := randomToken(32)
|
||
|
|
_, err := f.s.pool.Exec(f.ctx, `INSERT INTO onboarding_account_session(token_hash,csrf_hash,user_id,expires_at) SELECT $1,$2,id,now()+interval '1 hour' FROM user_account WHERE oidc_issuer=$3 AND oidc_subject=$4`, digest([]byte(session)), digest([]byte(webBLECSRF)), p.Issuer, p.Subject)
|
||
|
|
must(t, err)
|
||
|
|
return session
|
||
|
|
}
|
||
|
|
|
||
|
|
func webBLERequest(t *testing.T, s *Service, endpoint, session, csrf, origin string, in any) (int, map[string]any) {
|
||
|
|
t.Helper()
|
||
|
|
raw, _ := json.Marshal(in)
|
||
|
|
r := httptest.NewRequest("POST", "https://platform.example/api/v1/web/"+endpoint, bytes.NewReader(raw))
|
||
|
|
r.Header.Set("Origin", origin)
|
||
|
|
r.Header.Set("Content-Type", "application/json")
|
||
|
|
r.Header.Set("X-Request-ID", uuid.NewString())
|
||
|
|
r.Header.Set("X-CSRF-Token", csrf)
|
||
|
|
if session != "" {
|
||
|
|
r.AddCookie(&http.Cookie{Name: s.cookieName(true), Value: session})
|
||
|
|
}
|
||
|
|
w := httptest.NewRecorder()
|
||
|
|
mux := http.NewServeMux()
|
||
|
|
s.RegisterRoutes(mux)
|
||
|
|
mux.ServeHTTP(w, r)
|
||
|
|
var out map[string]any
|
||
|
|
must(t, json.Unmarshal(w.Body.Bytes(), &out))
|
||
|
|
data, _ := out["data"].(map[string]any)
|
||
|
|
return w.Code, data
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestWebBLEStateRequiresOwnerOriginAndCSRF(t *testing.T) {
|
||
|
|
f := serverFixture(t)
|
||
|
|
f.web.Hash = ""
|
||
|
|
p := accountOnlyUser(t, f)
|
||
|
|
session := webBLESession(t, f, p)
|
||
|
|
in := BLEDeviceInput{DeviceID: f.d.FactoryID}
|
||
|
|
for _, endpoint := range []string{"ble-device-state", "device-subject", "ble-bind-tokens"} {
|
||
|
|
for _, auth := range [][3]string{{"", webBLECSRF, "https://platform.example"}, {session, "wrong", "https://platform.example"}, {session, webBLECSRF, "https://evil.example"}} {
|
||
|
|
status, _ := webBLERequest(t, f.s, endpoint, auth[0], auth[1], auth[2], in)
|
||
|
|
if status != 401 && status != 403 {
|
||
|
|
t.Fatalf("%s allowed invalid auth: %d", endpoint, status)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
status, out := webBLERequest(t, f.s, "ble-bind-tokens", session, webBLECSRF, "https://platform.example", BLETokenInput{DeviceID: f.d.FactoryID})
|
||
|
|
if status != 200 {
|
||
|
|
t.Fatal("account-first token", status, out)
|
||
|
|
}
|
||
|
|
_, err := f.s.BindBLE(f.ctx, f.d, uuid.NewString(), BLEBindInput{Token: out["bind_token"].(string)})
|
||
|
|
must(t, err)
|
||
|
|
f.refresh()
|
||
|
|
generation := f.d.Generation
|
||
|
|
status, out = webBLERequest(t, f.s, "ble-device-state", session, webBLECSRF, "https://platform.example", in)
|
||
|
|
if status != 200 || out["bound"] != true || out["id"] != f.d.PublicID {
|
||
|
|
t.Fatal(status, out)
|
||
|
|
}
|
||
|
|
other := webBLESession(t, f, accountOnlyUser(t, f))
|
||
|
|
status, _ = webBLERequest(t, f.s, "ble-device-state", other, webBLECSRF, "https://platform.example", in)
|
||
|
|
if status != 403 {
|
||
|
|
t.Fatal("other account can reconfigure", status)
|
||
|
|
}
|
||
|
|
status, _ = webBLERequest(t, f.s, "ble-bind-tokens", session, webBLECSRF, "https://platform.example", BLETokenInput{DeviceID: f.d.FactoryID})
|
||
|
|
if status != 409 {
|
||
|
|
t.Fatal("bound device received another claim", status)
|
||
|
|
}
|
||
|
|
f.refresh()
|
||
|
|
if f.d.Generation != generation {
|
||
|
|
t.Fatal("Wi-Fi eligibility changed ownership")
|
||
|
|
}
|
||
|
|
}
|