diff --git a/server/http_test.go b/server/http_test.go index 0b6c179fce05719c1d77133eb432f42799148b16..61776987a541698650685f0ab6a34a5cc748ef8e 100644 --- a/server/http_test.go +++ b/server/http_test.go @@ -142,6 +142,12 @@ func checkStatusOk(t testing.TB, resp *http.Response) { } } +func checkStatusNotFound(t testing.TB, resp *http.Response) { + if resp.StatusCode != 404 { + t.Fatalf("expected status 404, got %s", resp.Status) + } + +} func checkRedirectToTargetService(t testing.TB, resp *http.Response) { if resp.StatusCode != 302 { t.Fatalf("expected status 302, got %s", resp.Status) @@ -180,10 +186,13 @@ func checkTargetSSOTicket(config *Config) func(testing.TB, *http.Response) { var usernameFieldRx = regexp.MustCompile(`<input[^>]*name="username"`) -func checkLoginPasswordPage(t testing.TB, resp *http.Response) { +func checkLoginPageURL(t testing.TB, resp *http.Response) { if resp.Request.URL.Path != "/login" { t.Errorf("request path is not /login (%s)", resp.Request.URL.String()) } +} + +func checkLoginPasswordPage(t testing.TB, resp *http.Response) { data, err := ioutil.ReadAll(resp.Body) if err != nil { t.Fatalf("reading body: %v", err) @@ -314,6 +323,10 @@ func TestHTTP_LoginOTP(t *testing.T) { v.Set("password", "password") doPostForm(t, httpSrv, c, "/login", v, checkStatusOk, checkLoginOTPPage) + // Make a request for a URL that does not exist, browsers might do this + // for a number of reasons. + doGet(t, c, httpSrv.URL+"/apple-iphone-special-icon.ico", checkStatusNotFound) + // Submit the correct OTP token. We expect the result to be a // 302 redirect to the target service. v = make(url.Values) diff --git a/server/integration_test.go b/server/integration_test.go new file mode 100644 index 0000000000000000000000000000000000000000..d1cbace9b7ad4bebb8e95ba0ebe4932330e0ba69 --- /dev/null +++ b/server/integration_test.go @@ -0,0 +1,145 @@ +package server + +import ( + "io/ioutil" + "net/http" + "net/http/httptest" + "net/url" + "os" + "path/filepath" + "strings" + "testing" + + "git.autistici.org/id/go-sso/httpsso" + "github.com/gorilla/securecookie" +) + +// Create a SSO-wrapped service. +func createTestProtectedService(t testing.TB, serverURL, tmpdir string) *httptest.Server { + ssoPubKey, err := ioutil.ReadFile(filepath.Join(tmpdir, "public")) + if err != nil { + t.Fatalf("oops, can't read sso public key: %v", err) + } + + w, err := httpsso.NewSSOWrapper( + serverURL, + ssoPubKey, + "example.com", + securecookie.GenerateRandomKey(64), + securecookie.GenerateRandomKey(32), + 0, + ) + if err != nil { + t.Fatalf("NewSSOWrapper(): %v", err) + } + + h := w.Wrap(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Write([]byte("OK")) // nolint + }), "service.example.com/", nil) + + return httptest.NewTLSServer(h) +} + +func startTestHTTPServerAndApp(t testing.TB) (string, *httptest.Server, *httptest.Server) { + tmpdir, _ := ioutil.TempDir("", "") + config := testConfig(t, tmpdir, "") + srv := createTestHTTPServer(t, config) + app := createTestProtectedService(t, "https://login.example.com/", tmpdir) + return tmpdir, srv, app +} + +func startTestHTTPServerWithPrefixAndApp(t testing.TB) (string, *httptest.Server, *httptest.Server) { + tmpdir, _ := ioutil.TempDir("", "") + config := testConfig(t, tmpdir, "") + config.URLPrefix = "/sso" + srv := createTestHTTPServer(t, config) + app := createTestProtectedService(t, "https://login.example.com/sso", tmpdir) + return tmpdir, srv, app +} + +func checkLoginPageURLWithPrefix(t testing.TB, resp *http.Response) { + if resp.Request.URL.Path != "/sso/login" { + t.Errorf("request path is not /sso/login (%s)", resp.Request.URL.String()) + } +} + +func checkIsProtectedService(t testing.TB, resp *http.Response) { + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatalf("reading body: %v", err) + } + if s := string(data); s != "OK" { + t.Fatalf("not the target application, response body='%s'", s) + } +} + +func checkLogoutPageHasLinks(t testing.TB, resp *http.Response) { + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatalf("reading body: %v", err) + } + logoutURL := "https://service.example.com/sso_logout" + if sdata := string(data); !strings.Contains(sdata, logoutURL) { + t.Fatalf("service logout URL not found in logout page:\n%s", sdata) + } +} + +func addrFromURL(s string) string { + u, _ := url.Parse(s) + if !strings.Contains(u.Host, ":") { + return u.Host + ":443" + } + return u.Host +} + +// The integration test spins up an actual service and verifies the +// interaction between it and the login application, using DNS-level +// overrides to ensure proper name validation. +func TestIntegration(t *testing.T) { + tmpdir, srv, app := startTestHTTPServerAndApp(t) + defer os.RemoveAll(tmpdir) + defer srv.Close() + defer app.Close() + + c := makeHTTPClient(map[string]string{ + "login.example.com:443": addrFromURL(srv.URL), + "service.example.com:443": addrFromURL(app.URL), + }, true) + + doGet(t, c, "https://service.example.com/", checkStatusOk, checkLoginPageURL, checkLoginPasswordPage) + + v := make(url.Values) + v.Set("username", "testuser") + v.Set("password", "password") + doPostForm(t, c, "https://login.example.com/login", v, checkStatusOk, checkIsProtectedService) + + // Now attempt to logout, and verify that we can't access the service anymore. + doGet(t, c, "https://login.example.com/logout", checkStatusOk, checkLogoutPageHasLinks) + doGet(t, c, "https://service.example.com/sso_logout", checkStatusOk) + doGet(t, c, "https://service.example.com/", checkStatusOk, checkLoginPageURL, checkLoginPasswordPage) +} + +// Same test as above, but the server application has a URL prefix. +func TestIntegration_WithURLPrefix(t *testing.T) { + tmpdir, srv, app := startTestHTTPServerWithPrefixAndApp(t) + defer os.RemoveAll(tmpdir) + defer srv.Close() + defer app.Close() + + c := makeHTTPClient(map[string]string{ + "login.example.com:443": addrFromURL(srv.URL), + "service.example.com:443": addrFromURL(app.URL), + }, true) + + doGet(t, c, "https://service.example.com/", checkStatusOk, checkLoginPageURLWithPrefix, checkLoginPasswordPage) + + v := make(url.Values) + v.Set("username", "testuser") + v.Set("password", "password") + doPostForm(t, c, "https://login.example.com/sso/login", v, checkStatusOk, checkIsProtectedService) + + // Now attempt to logout, and verify that we can't access the service anymore. + doGet(t, c, "https://login.example.com/sso/logout", checkStatusOk, checkLogoutPageHasLinks) + doGet(t, c, "https://service.example.com/sso_logout", checkStatusOk) + doGet(t, c, "https://service.example.com/", checkStatusOk, checkLoginPageURLWithPrefix, checkLoginPasswordPage) +}