diff --git a/server/http_test.go b/server/http_test.go index a9cdf13cd086bf85bdc291b39c2e1687b2768a66..60864b008f05111676ac8fa1c18c68831aa31215 100644 --- a/server/http_test.go +++ b/server/http_test.go @@ -7,6 +7,7 @@ import ( "errors" "io" "io/ioutil" + "net" "net/http" "net/http/cookiejar" "net/http/httptest" @@ -68,12 +69,23 @@ func startTestHTTPServerWithKeyStore(t testing.TB) (string, *httptest.Server) { return tmpdir, createTestHTTPServer(t, config) } -func newTestHTTPClient() *http.Client { +func makeHTTPClient(dnsOverrides map[string]string, followExternalRedirects bool) *http.Client { jar, _ := cookiejar.New(nil) + var dialfn func(ctx context.Context, network, addr string) (net.Conn, error) + if dnsOverrides != nil { + dialer := new(net.Dialer) + dialfn = func(ctx context.Context, network, addr string) (net.Conn, error) { + if override, ok := dnsOverrides[addr]; ok { + addr = override + } + return dialer.DialContext(ctx, network, addr) + } + } transport := NewLoggedTransport(&http.Transport{ TLSClientConfig: &tls.Config{ InsecureSkipVerify: true, }, + DialContext: dialfn, }, DefaultLogger{Dump: true}) return &http.Client{ Jar: jar, @@ -83,7 +95,7 @@ func newTestHTTPClient() *http.Client { if len(via) > 10 { return errors.New("too many redirects") } - if !strings.HasPrefix(req.URL.Host, "127.0.0.1:") { + if !followExternalRedirects && !strings.HasPrefix(req.URL.Host, "127.0.0.1:") { return http.ErrUseLastResponse } return nil @@ -91,6 +103,10 @@ func newTestHTTPClient() *http.Client { } } +func newTestHTTPClient() *http.Client { + return makeHTTPClient(nil, false) +} + func TestHTTP_ServeStaticAsset(t *testing.T) { tmpdir, httpSrv := startTestHTTPServer(t) defer os.RemoveAll(tmpdir) @@ -106,10 +122,10 @@ func TestHTTP_ServeStaticAsset(t *testing.T) { } } -func doGet(t testing.TB, srv *httptest.Server, c *http.Client, relativeURL string, checkResponse ...func(testing.TB, *http.Response)) { - resp, err := c.Get(srv.URL + relativeURL) +func doGet(t testing.TB, c *http.Client, uri string, checkResponse ...func(testing.TB, *http.Response)) { + resp, err := c.Get(uri) if err != nil { - t.Fatalf("http.Get(%s): %v", relativeURL, err) + t.Fatalf("http.Get(%s): %v", uri, err) } defer resp.Body.Close() for _, f := range checkResponse { @@ -117,10 +133,10 @@ func doGet(t testing.TB, srv *httptest.Server, c *http.Client, relativeURL strin } } -func doPostForm(t testing.TB, srv *httptest.Server, c *http.Client, relativeURL string, v url.Values, checkResponse ...func(testing.TB, *http.Response)) { - resp, err := c.PostForm(srv.URL+relativeURL, v) +func doPostForm(t testing.TB, c *http.Client, uri string, v url.Values, checkResponse ...func(testing.TB, *http.Response)) { + resp, err := c.PostForm(uri, v) if err != nil { - t.Fatalf("http.Get(%s): %v", relativeURL, err) + t.Fatalf("http.Get(%s): %v", uri, err) } defer resp.Body.Close() for _, f := range checkResponse { @@ -173,6 +189,21 @@ func checkLoginOTPPage(t testing.TB, resp *http.Response) { } } +func checkLogoutPage(t testing.TB, resp *http.Response) { + if resp.Request.URL.Path != "/logout" { + t.Errorf("request path is not /logout (%s)", resp.Request.URL.String()) + } + + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatalf("reading body: %v", err) + } + + if s := string(data); !strings.Contains(s, "Signing you out from all services") { + t.Fatalf("not the logout page:\n%s", s) + } +} + func TestHTTP_Login(t *testing.T) { tmpdir, httpSrv := startTestHTTPServer(t) defer os.RemoveAll(tmpdir) @@ -186,14 +217,14 @@ func TestHTTP_Login(t *testing.T) { v.Set("s", "service.example.com/") v.Set("d", "https://service.example.com/admin/") v.Set("n", "averysecretnonce") - doGet(t, httpSrv, c, "/?"+v.Encode(), checkStatusOk, checkLoginPasswordPage) + doGet(t, c, httpSrv.URL+"/?"+v.Encode(), checkStatusOk, checkLoginPasswordPage) // Attempt to login by submitting the form. We expect the // result to be a 302 redirect to the target service. v = make(url.Values) v.Set("username", "testuser") v.Set("password", "password") - doPostForm(t, httpSrv, c, "/login", v, checkRedirectToTargetService) + doPostForm(t, c, httpSrv.URL+"/login", v, checkRedirectToTargetService) } func TestHTTP_LoginOnSecondAttempt(t *testing.T) { @@ -209,20 +240,20 @@ func TestHTTP_LoginOnSecondAttempt(t *testing.T) { v.Set("s", "service.example.com/") v.Set("d", "https://service.example.com/admin/") v.Set("n", "averysecretnonce") - doGet(t, httpSrv, c, "/?"+v.Encode(), checkStatusOk, checkLoginPasswordPage) + doGet(t, c, httpSrv.URL+"/?"+v.Encode(), checkStatusOk, checkLoginPasswordPage) // Attempt to login with wrong credentials. v = make(url.Values) v.Set("username", "testuser") v.Set("password", "badpassword") - doPostForm(t, httpSrv, c, "/login", v, checkStatusOk, checkLoginPasswordPage) + doPostForm(t, c, httpSrv.URL+"/login", v, checkStatusOk, checkLoginPasswordPage) // Attempt to login by submitting the form. We expect the // result to be a 302 redirect to the target service. v = make(url.Values) v.Set("username", "testuser") v.Set("password", "password") - doPostForm(t, httpSrv, c, "/login", v, checkRedirectToTargetService) + doPostForm(t, c, httpSrv.URL+"/login", v, checkRedirectToTargetService) } func TestHTTP_LoginAndLogout(t *testing.T) { @@ -238,24 +269,24 @@ func TestHTTP_LoginAndLogout(t *testing.T) { v.Set("s", "service.example.com/") v.Set("d", "https://service.example.com/admin/") v.Set("n", "averysecretnonce") - doGet(t, httpSrv, c, "/?"+v.Encode(), checkStatusOk, checkLoginPasswordPage) + doGet(t, c, httpSrv.URL+"/?"+v.Encode(), checkStatusOk, checkLoginPasswordPage) // Attempt to login by submitting the form. We expect the // result to be a 302 redirect to the target service. v = make(url.Values) v.Set("username", "testuser") v.Set("password", "password") - doPostForm(t, httpSrv, c, "/login", v, checkRedirectToTargetService) + doPostForm(t, c, httpSrv.URL+"/login", v, checkRedirectToTargetService) // Make a logout request. - doGet(t, httpSrv, c, "/logout", checkStatusOk) + doGet(t, c, httpSrv.URL+"/logout", checkStatusOk, checkLogoutPage) // This new authorization request should send us to the login page. v = make(url.Values) v.Set("s", "service.example.com/") v.Set("d", "https://service.example.com/admin/") v.Set("n", "averysecretnonce") - doGet(t, httpSrv, c, "/?"+v.Encode(), checkStatusOk, checkLoginPasswordPage) + doGet(t, c, httpSrv.URL+"/?"+v.Encode(), checkStatusOk, checkLoginPasswordPage) } func TestHTTP_LoginOTP(t *testing.T) { @@ -271,19 +302,19 @@ func TestHTTP_LoginOTP(t *testing.T) { v.Set("s", "service.example.com/") v.Set("d", "https://service.example.com/admin/") v.Set("n", "averysecretnonce") - doGet(t, httpSrv, c, "/?"+v.Encode(), checkStatusOk, checkLoginPasswordPage) + doGet(t, c, httpSrv.URL+"/?"+v.Encode(), checkStatusOk, checkLoginPasswordPage) // Attempt to login by submitting the form. We should see the OTP page. v = make(url.Values) v.Set("username", "test2fa") v.Set("password", "password") - doPostForm(t, httpSrv, c, "/login", v, checkStatusOk, checkLoginOTPPage) + doPostForm(t, c, httpSrv.URL+"/login", v, checkStatusOk, checkLoginOTPPage) // Submit the correct OTP token. We expect the result to be a // 302 redirect to the target service. v = make(url.Values) v.Set("otp", "123456") - doPostForm(t, httpSrv, c, "/login/otp", v, checkRedirectToTargetService) + doPostForm(t, c, httpSrv.URL+"/login/otp", v, checkRedirectToTargetService) } func createFakeKeyStore(t testing.TB, username, password string) *httptest.Server { @@ -322,14 +353,14 @@ func TestHTTP_LoginWithKeyStore(t *testing.T) { v.Set("s", "service.example.com/") v.Set("d", "https://service.example.com/admin/") v.Set("n", "averysecretnonce") - doGet(t, httpSrv, c, "/?"+v.Encode(), checkStatusOk, checkLoginPasswordPage) + doGet(t, c, httpSrv.URL+"/?"+v.Encode(), checkStatusOk, checkLoginPasswordPage) // Attempt to login by submitting the form. We expect the // result to be a 302 redirect to the target service. v = make(url.Values) v.Set("username", "testuser") v.Set("password", "password") - doPostForm(t, httpSrv, c, "/login", v, checkRedirectToTargetService) + doPostForm(t, c, httpSrv.URL+"/login", v, checkRedirectToTargetService) } func TestHTTP_CORS(t *testing.T) { @@ -346,14 +377,14 @@ func TestHTTP_CORS(t *testing.T) { v.Set("s", "service.example.com/") v.Set("d", "https://service.example.com/admin/") v.Set("n", "averysecretnonce") - doGet(t, httpSrv, c, "/?"+v.Encode(), checkStatusOk, checkLoginPasswordPage) + doGet(t, c, httpSrv.URL+"/?"+v.Encode(), checkStatusOk, checkLoginPasswordPage) // Attempt to login by submitting the form. We expect the // result to be a 302 redirect to the target service. v = make(url.Values) v.Set("username", "testuser") v.Set("password", "password") - doPostForm(t, httpSrv, c, "/login", v, checkRedirectToTargetService) + doPostForm(t, c, httpSrv.URL+"/login", v, checkRedirectToTargetService) // Simulate a CORS preflight request. v = make(url.Values) diff --git a/server/integration_test.go b/server/integration_test.go new file mode 100644 index 0000000000000000000000000000000000000000..930b62afb3862e1612af601c8430fbfd3519be95 --- /dev/null +++ b/server/integration_test.go @@ -0,0 +1,105 @@ +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 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, 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, checkLoginPasswordPage) +}