package callscore import ( "context" "crypto/sha256" "encoding/base64" "encoding/json" "net/http" "net/http/httptest" "strings" "sync/atomic" "testing" "time" ) func TestAuthenticationProofAndCancellation(t *testing.T) { id := base64.RawURLEncoding.EncodeToString(make([]byte, 32)) challengeReceived := make(chan string, 1) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Header.Get("Cookie") != "" { t.Error("authentication sent ambient credentials") } var in map[string]string json.NewDecoder(r.Body).Decode(&in) if r.URL.Path != "/native/request" || !authToken(in["challenge"]) || in["target"] != "/m/abc" { t.Error("invalid authentication request") } challengeReceived <- in["challenge"] json.NewEncoder(w).Encode(map[string]string{"id": id}) })) defer server.Close() auth, err := BeginAuthentication(t.Context(), server.URL+"/m/abc/join") if err != nil { t.Fatal(err) } challenge := <-challengeReceived sum := sha256.Sum256([]byte(auth.verifier)) if challenge != base64.RawURLEncoding.EncodeToString(sum[:]) { t.Fatal("challenge does not bind proof") } if strings.Contains(auth.BrowserURL(), auth.verifier) || strings.Contains(auth.BrowserURL(), challenge) { t.Fatal("proof leaked into browser URL") } if auth.ConfirmationCode() == "" { t.Fatal("missing comparison code") } // Cancellation must interrupt the wait promptly, including while idle between polls. auth.Cancel() done := make(chan error, 1) go func() { _, e := auth.Wait(); done <- e }() select { case e := <-done: if e == nil { t.Fatal("cancel succeeded") } case <-time.After(time.Second): t.Fatal("cancel did not stop polling") } } func TestAuthenticationNeverFollowsRedirect(t *testing.T) { var calls atomic.Int32 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { calls.Add(1); http.Redirect(w, r, "/signin", 303) })) defer server.Close() if _, err := BeginAuthentication(t.Context(), server.URL+"/m/abc"); err == nil { t.Fatal("old server accepted") } if calls.Load() != 1 { t.Fatal("followed authentication redirect") } } func TestAuthenticationRejectsUnsafeAddress(t *testing.T) { for _, address := range []string{"http://example.test/m/abc", "https://alice:secret@example.test/m/abc", "https://example.test/m/abc?token=secret", "https://example.test/j/a#secret", "https://example.test/signin", "https://example.test/j/%2f"} { if _, err := BeginAuthentication(context.Background(), address); err == nil { t.Errorf("accepted %q", address) } } } func TestAuthenticationWithoutAPathTargetsTheMeetingList(t *testing.T) { // The person who has no link, because the meeting they are about to be // in does not exist yet. The target is the list, and the result carries // no room: the host reads that as "signed in, nowhere yet". var target atomic.Value server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var in map[string]string json.NewDecoder(r.Body).Decode(&in) switch r.URL.Path { case "/native/request": target.Store(in["target"]) json.NewEncoder(w).Encode(map[string]string{"id": strings.Repeat("A", 43)}) case "/native/exchange": json.NewEncoder(w).Encode(map[string]string{ // 43 base64url zeros: the only 43-character run that both // decodes to thirty-two bytes and re-encodes to itself, // which is what authToken insists on. "name": "rastrillo_session", "value": strings.Repeat("A", 43), "target": "/", }) } })) defer server.Close() for _, address := range []string{server.URL, server.URL + "/"} { a, err := BeginAuthentication(t.Context(), address) if err != nil { t.Fatal(address, err) } if target.Load() != "/" { t.Fatal("wrong target", target.Load()) } result, err := a.Wait() if err != nil { t.Fatal(err) } if result.Room != "" { t.Fatal("a list sign-in named a meeting", result.Room) } if len(result.Cookies) != 1 { t.Fatal("no session") } } } func TestAuthenticationStillRejectsAPathThatIsNotAMeeting(t *testing.T) { // Widening the empty path to mean "the instance" must not widen // anything else: a path that is present and wrong is still an error, // not a quiet fallback to the list. for _, address := range []string{"https://example.test/settings", "https://example.test/m", "https://example.test/m/a/b", "https://example.test/x/abc"} { if _, err := BeginAuthentication(t.Context(), address); err == nil { t.Errorf("accepted %q", address) } } }