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)
+}