diff --git a/server/http_test.go b/server/http_test.go
index b3a5e267f8ea748282089febbce4ebde411477a7..299623a5b603335511eb9c9e03b53c6938f1121d 100644
--- a/server/http_test.go
+++ b/server/http_test.go
@@ -158,12 +158,18 @@ func checkStatusOk(t testing.TB, resp *http.Response) {
 	}
 }
 
+func checkStatusForbidden(t testing.TB, resp *http.Response) {
+	if resp.StatusCode != 403 {
+		t.Fatalf("expected status 403, got %s", resp.Status)
+	}
+}
+
 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)
@@ -260,6 +266,16 @@ func checkLogoutPage(t testing.TB, resp *http.Response) {
 	}
 }
 
+func extractSSOTicket(dest *string) func(testing.TB, *http.Response) {
+	return func(t testing.TB, resp *http.Response) {
+		u, err := url.Parse(resp.Header.Get("Location"))
+		if err != nil {
+			t.Fatalf("could not parse Location URL: %v", err)
+		}
+		*dest = u.Query().Get("t")
+	}
+}
+
 func TestHTTP_Login(t *testing.T) {
 	tmpdir, httpSrv, config := startTestHTTPServerWithConfig(t)
 	defer os.RemoveAll(tmpdir)
@@ -527,3 +543,45 @@ func TestHTTP_CORS(t *testing.T) {
 		t.Fatalf("Bad Access-Control-Allow-Origin returned to OPTIONS request: %s", s)
 	}
 }
+
+func TestHTTP_LoginAndExchange(t *testing.T) {
+	tmpdir, httpSrv := startTestHTTPServer(t)
+	defer os.RemoveAll(tmpdir)
+	defer httpSrv.Close()
+
+	c := newTestHTTPClient()
+
+	// Simulate an authorization request from a service, expect to
+	// see 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, c, httpSrv.URL+"/?"+v.Encode(), checkStatusOk, checkLoginPageURL, 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")
+	var ssoTkt string
+	doPostForm(t, c, httpSrv.URL+"/login", v, checkRedirectToTargetService, extractSSOTicket(&ssoTkt))
+
+	// Make an exchange request for a new service.
+	v = make(url.Values)
+	v.Set("cur_tkt", ssoTkt)
+	v.Set("cur_svc", "service.example.com/")
+	v.Set("cur_nonce", "averysecretnonce")
+	v.Set("new_svc", "service2.example.com/")
+	v.Set("new_nonce", "anothernonce")
+	doPostForm(t, c, httpSrv.URL+"/exchange", v, checkStatusOk)
+
+	// Make an exchange request for a forbidden service.
+	v = make(url.Values)
+	v.Set("cur_tkt", ssoTkt)
+	v.Set("cur_svc", "service.example.com/")
+	v.Set("cur_nonce", "averysecretnonce")
+	v.Set("new_svc", "service3.example.com/")
+	v.Set("new_nonce", "anothernonce")
+	doPostForm(t, c, httpSrv.URL+"/exchange", v, checkStatusForbidden)
+}
diff --git a/server/service_test.go b/server/service_test.go
index 6829ff49f249bc0f9d115214cda40660ceafedec..f6a740fd591884416581f673dfcc015c3e9fc15a 100644
--- a/server/service_test.go
+++ b/server/service_test.go
@@ -39,9 +39,10 @@ domain: example.com
 allowed_services:
   - "^service\\.example\\.com/$"
   - "^service2\\.example\\.com/$"
+  - "^service3\\.example\\.com/$"
 allowed_exchanges:
   - src_regexp: "^service\\.example\\.com/$"
-    dst_regexp: "\\.example\\.com/.*$"
+    dst_regexp: "^service2\\.example\\.com/$"
 allowed_cors_origins:
   - "https://origin.example.com"
 service_ttls:
@@ -139,9 +140,10 @@ func TestLoginService_Exchange(t *testing.T) {
 		service, destination string
 		ok                   bool
 	}{
+		{"service.example.com/", "service.example.com/", false},
 		{"service.example.com/", "service2.example.com/", true},
+		{"service.example.com/", "service3.example.com/", false},
 		{"service.example.com/", "bad-service.another.com/", false},
-		{"service.example.com/", "service.example.com/", true}, // self-exchange??
 	}
 
 	for _, td := range testdata {