http_test.go 18.1 KB
Newer Older
ale's avatar
ale committed
1
2
3
4
5
package server

import (
	"context"
	"crypto/tls"
6
	"encoding/json"
ale's avatar
ale committed
7
	"errors"
8
	"io"
ale's avatar
ale committed
9
	"io/ioutil"
ale's avatar
ale committed
10
	"net"
ale's avatar
ale committed
11
12
13
14
15
16
17
18
19
20
	"net/http"
	"net/http/cookiejar"
	"net/http/httptest"
	"net/url"
	"os"
	"regexp"
	"strings"
	"testing"

	"git.autistici.org/id/auth"
21
	"git.autistici.org/id/keystore"
ale's avatar
ale committed
22
23
24
25
26
27
)

type fakeAuthClient struct{}

func (c *fakeAuthClient) Authenticate(_ context.Context, req *auth.Request) (*auth.Response, error) {
	p := string(req.Password)
28
29
30
31
	info := &auth.UserInfo{
		Shard:  "shard1",
		Groups: []string{"users"},
	}
ale's avatar
ale committed
32
33
	switch {
	case req.Username == "testuser" && p == "password":
34
		return &auth.Response{Status: auth.StatusOK, UserInfo: info}, nil
ale's avatar
ale committed
35
	case req.Username == "test2fa" && p == "password" && req.OTP == "123456":
36
		return &auth.Response{Status: auth.StatusOK, UserInfo: info}, nil
ale's avatar
ale committed
37
38
	case req.Username == "test2fa" && p == "password":
		return &auth.Response{
39
40
			Status:     auth.StatusInsufficientCredentials,
			TFAMethods: []auth.TFAMethod{auth.TFAMethodOTP},
ale's avatar
ale committed
41
42
43
44
45
46
		}, nil
	}

	return &auth.Response{Status: auth.StatusError}, nil
}

47
func createTestHTTPServer(t testing.TB, config *Config) *httptest.Server {
ale's avatar
ale committed
48
49
50
51
52
53
54
55
56
57
	svc, err := NewLoginService(config)
	if err != nil {
		t.Fatal("NewLoginService():", err)
	}

	srv, err := New(svc, &fakeAuthClient{}, config)
	if err != nil {
		t.Fatal("New():", err)
	}

58
59
60
	return httptest.NewTLSServer(srv.Handler())
}

61
func startTestHTTPServerWithConfig(t testing.TB) (string, *httptest.Server, *Config) {
62
63
	tmpdir, _ := ioutil.TempDir("", "")
	config := testConfig(t, tmpdir, "")
64
65
66
67
68
69
	return tmpdir, createTestHTTPServer(t, config), config
}

func startTestHTTPServer(t testing.TB) (string, *httptest.Server) {
	tmpdir, srv, _ := startTestHTTPServerWithConfig(t)
	return tmpdir, srv
70
71
}

ale's avatar
ale committed
72
func startTestHTTPServerWithKeyStore(t testing.TB) (string, *httptest.Server, *fakeKeyStore) {
73
74
75
76
	ks := createFakeKeyStore(t, "testuser", "password")

	tmpdir, _ := ioutil.TempDir("", "")
	config := testConfig(t, tmpdir, ks.URL)
ale's avatar
ale committed
77
	return tmpdir, createTestHTTPServer(t, config), ks
ale's avatar
ale committed
78
79
}

ale's avatar
ale committed
80
func makeHTTPClient(dnsOverrides map[string]string, followExternalRedirects bool) *http.Client {
ale's avatar
ale committed
81
	jar, _ := cookiejar.New(nil)
ale's avatar
ale committed
82
83
84
85
86
87
88
89
90
91
	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)
		}
	}
ale's avatar
ale committed
92
93
94
95
	transport := NewLoggedTransport(&http.Transport{
		TLSClientConfig: &tls.Config{
			InsecureSkipVerify: true,
		},
ale's avatar
ale committed
96
		DialContext: dialfn,
ale's avatar
ale committed
97
98
99
100
101
102
103
104
105
	}, DefaultLogger{Dump: true})
	return &http.Client{
		Jar:       jar,
		Transport: transport,
		// This client will only follow redirects to localhost.
		CheckRedirect: func(req *http.Request, via []*http.Request) error {
			if len(via) > 10 {
				return errors.New("too many redirects")
			}
ale's avatar
ale committed
106
			if !followExternalRedirects && !strings.HasPrefix(req.URL.Host, "127.0.0.1:") {
ale's avatar
ale committed
107
108
109
110
111
112
113
				return http.ErrUseLastResponse
			}
			return nil
		},
	}
}

ale's avatar
ale committed
114
115
116
117
func newTestHTTPClient() *http.Client {
	return makeHTTPClient(nil, false)
}

ale's avatar
ale committed
118
119
120
121
122
123
func TestHTTP_ServeStaticAsset(t *testing.T) {
	tmpdir, httpSrv := startTestHTTPServer(t)
	defer os.RemoveAll(tmpdir)
	defer httpSrv.Close()

	c := newTestHTTPClient()
ale's avatar
ale committed
124
	resp, err := c.Get(httpSrv.URL + "/static/js/u2f.js")
ale's avatar
ale committed
125
126
127
128
129
130
131
132
	if err != nil {
		t.Fatal("http.Get():", err)
	}
	if resp.StatusCode != 200 {
		t.Fatalf("bad status: %s", resp.Status)
	}
}

ale's avatar
ale committed
133
134
func doGet(t testing.TB, c *http.Client, uri string, checkResponse ...func(testing.TB, *http.Response)) {
	resp, err := c.Get(uri)
ale's avatar
ale committed
135
	if err != nil {
ale's avatar
ale committed
136
		t.Fatalf("http.Get(%s): %v", uri, err)
ale's avatar
ale committed
137
138
139
140
141
142
143
	}
	defer resp.Body.Close()
	for _, f := range checkResponse {
		f(t, resp)
	}
}

ale's avatar
ale committed
144
145
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)
ale's avatar
ale committed
146
	if err != nil {
ale's avatar
ale committed
147
		t.Fatalf("http.Get(%s): %v", uri, err)
ale's avatar
ale committed
148
149
150
151
152
153
154
155
156
157
158
159
160
	}
	defer resp.Body.Close()
	for _, f := range checkResponse {
		f(t, resp)
	}
}

func checkStatusOk(t testing.TB, resp *http.Response) {
	if resp.StatusCode != 200 {
		t.Fatalf("expected status 200, got %s", resp.Status)
	}
}

161
162
163
164
165
166
func checkStatusForbidden(t testing.TB, resp *http.Response) {
	if resp.StatusCode != 403 {
		t.Fatalf("expected status 403, got %s", resp.Status)
	}
}

167
168
169
170
171
func checkStatusNotFound(t testing.TB, resp *http.Response) {
	if resp.StatusCode != 404 {
		t.Fatalf("expected status 404, got %s", resp.Status)
	}
}
172

ale's avatar
ale committed
173
174
175
176
177
178
179
180
181
func checkRedirectToTargetService(t testing.TB, resp *http.Response) {
	if resp.StatusCode != 302 {
		t.Fatalf("expected status 302, got %s", resp.Status)
	}
	if !strings.HasPrefix(resp.Header.Get("Location"), "https://service.example.com/sso_login?") {
		t.Fatalf("redirect is not to target service: %v", resp.Header.Get("Location"))
	}
}

182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
func checkTargetSSOTicket(config *Config) 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)
		}
		tstr := u.Query().Get("t")
		nonce := u.Query().Get("n")

		// Validate the ticket in order to read it.
		v, err := newValidatorFromConfig(config)
		if err != nil {
			t.Fatalf("newValidatorFromConfig: %v", err)
		}
		ticket, err := v.Validate(tstr, nonce, "service.example.com/", nil)
		if err != nil {
			t.Fatalf("sso.Validate(%s): %v", tstr, err)
		}
		if n := len(ticket.Groups); n != 1 {
			t.Errorf("ticket has %d groups, expected 1", n)
		}
		if ticket.Groups[0] != "users" {
			t.Errorf("group is '%s', expected 'users'", ticket.Groups[0])
		}
	}
}

ale's avatar
ale committed
209
210
var usernameFieldRx = regexp.MustCompile(`<input[^>]*name="username"`)

211
func checkLoginPageURL(t testing.TB, resp *http.Response) {
ale's avatar
ale committed
212
213
214
	if resp.Request.URL.Path != "/login" {
		t.Errorf("request path is not /login (%s)", resp.Request.URL.String())
	}
215
216
217
}

func checkLoginPasswordPage(t testing.TB, resp *http.Response) {
ale's avatar
ale committed
218
219
220
221
222
223
224
225
226
227
228
229
	data, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		t.Fatalf("reading body: %v", err)
	}
	if !usernameFieldRx.Match(data) {
		t.Fatalf("not the password login page:\n%s", string(data))
	}
}

var otpFieldRx = regexp.MustCompile(`<input[^>]*name="otp"`)

func checkLoginOTPPage(t testing.TB, resp *http.Response) {
ale's avatar
ale committed
230
	if resp.Request.URL.Path != "/login/otp" {
ale's avatar
ale committed
231
232
233
234
235
236
237
238
239
240
241
		t.Errorf("request path is not /login (%s)", resp.Request.URL.String())
	}
	data, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		t.Fatalf("reading body: %v", err)
	}
	if !otpFieldRx.Match(data) {
		t.Fatalf("not the OTP login page:\n%s", string(data))
	}
}

ale's avatar
ale committed
242
243
244
245
246
247
248
249
250
251
252
253
var authFailureRx = regexp.MustCompile(`<p\s*class="error">\s*Authentication failed`)

func checkAuthFailure(t testing.TB, resp *http.Response) {
	data, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		t.Fatalf("reading body: %v", err)
	}
	if !authFailureRx.Match(data) {
		t.Fatalf("expected authentication failure, but no errors found:\n%s", string(data))
	}
}

ale's avatar
ale committed
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
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)
	}
}

269
270
271
272
273
274
275
276
277
278
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")
	}
}

ale's avatar
ale committed
279
func TestHTTP_Login(t *testing.T) {
280
	tmpdir, httpSrv, config := startTestHTTPServerWithConfig(t)
ale's avatar
ale committed
281
282
283
284
285
286
287
288
289
290
291
	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")
292
	v.Set("g", "users")
293
	doGet(t, c, httpSrv.URL+"/?"+v.Encode(), checkStatusOk, checkLoginPageURL, checkLoginPasswordPage)
ale's avatar
ale committed
294
295
296
297
298
299

	// 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")
300
	doPostForm(t, c, httpSrv.URL+"/login", v, checkRedirectToTargetService, checkTargetSSOTicket(config))
ale's avatar
ale committed
301
302
}

ale's avatar
ale committed
303
304
305
306
307
308
309
310
311
312
313
314
315
func TestHTTP_LoginOnSecondAttempt(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")
316
	doGet(t, c, httpSrv.URL+"/?"+v.Encode(), checkStatusOk, checkLoginPageURL, checkLoginPasswordPage)
ale's avatar
ale committed
317
318
319
320
321

	// Attempt to login with wrong credentials.
	v = make(url.Values)
	v.Set("username", "testuser")
	v.Set("password", "badpassword")
322
	doPostForm(t, c, httpSrv.URL+"/login", v, checkStatusOk, checkLoginPageURL, checkLoginPasswordPage)
ale's avatar
ale committed
323
324
325
326
327
328

	// 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")
ale's avatar
ale committed
329
	doPostForm(t, c, httpSrv.URL+"/login", v, checkRedirectToTargetService)
ale's avatar
ale committed
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
}

func TestHTTP_LoginAndLogout(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")
345
	doGet(t, c, httpSrv.URL+"/?"+v.Encode(), checkStatusOk, checkLoginPageURL, checkLoginPasswordPage)
ale's avatar
ale committed
346
347
348
349
350
351

	// 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")
ale's avatar
ale committed
352
	doPostForm(t, c, httpSrv.URL+"/login", v, checkRedirectToTargetService)
ale's avatar
ale committed
353
354

	// Make a logout request.
ale's avatar
ale committed
355
	doGet(t, c, httpSrv.URL+"/logout", checkStatusOk, checkLogoutPage)
ale's avatar
ale committed
356
357
358
359
360
361

	// 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")
362
	doGet(t, c, httpSrv.URL+"/?"+v.Encode(), checkStatusOk, checkLoginPageURL, checkLoginPasswordPage)
ale's avatar
ale committed
363
364
}

ale's avatar
ale committed
365
366
367
368
369
370
371
372
373
374
375
376
377
func TestHTTP_LoginOTP(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")
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
	doGet(t, c, httpSrv.URL+"/?"+v.Encode(), checkStatusOk, checkLoginPageURL, 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, 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, c, httpSrv.URL+"/login/otp", v, checkRedirectToTargetService)
}

ale's avatar
ale committed
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
func TestHTTP_LoginOTP_Fail(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 should see the OTP page.
	v = make(url.Values)
	v.Set("username", "test2fa")
	v.Set("password", "password")
	doPostForm(t, c, httpSrv.URL+"/login", v, checkStatusOk, checkLoginOTPPage)

	// Submit a bad OTP token, test for failure.
	v = make(url.Values)
	v.Set("otp", "000000")
	doPostForm(t, c, httpSrv.URL+"/login/otp", v, checkAuthFailure)
}

420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
func TestHTTP_LoginOTP_Intermediate404(t *testing.T) {
	// This test verifies that the session is not disrupted by a
	// request for a URL that does not exist during a 2FA login
	// workflow. The point is that the 404 should *not* Reset()
	// the session.
	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)
ale's avatar
ale committed
438
439
440
441
442

	// 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")
ale's avatar
ale committed
443
	doPostForm(t, c, httpSrv.URL+"/login", v, checkStatusOk, checkLoginOTPPage)
ale's avatar
ale committed
444

445
446
447
448
	// 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)

ale's avatar
ale committed
449
450
451
452
	// 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")
ale's avatar
ale committed
453
	doPostForm(t, c, httpSrv.URL+"/login/otp", v, checkRedirectToTargetService)
ale's avatar
ale committed
454
}
455

ale's avatar
ale committed
456
457
458
459
460
461
462
type fakeKeyStore struct {
	*httptest.Server
	values map[string]string
}

func createFakeKeyStore(t testing.TB, username, password string) *fakeKeyStore {
	values := make(map[string]string)
463
464
465
466
467
468
469
470
471
472
	h := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
		if req.URL.Path != "/api/open" {
			http.NotFound(w, req)
			return
		}
		var openReq keystore.OpenRequest
		if err := json.NewDecoder(req.Body).Decode(&openReq); err != nil {
			t.Errorf("bad JSON body: %v", err)
			return
		}
ale's avatar
ale committed
473
		values[openReq.Username] = openReq.Password
474
		w.Header().Set("Content-Type", "application/json")
ale's avatar
ale committed
475
		io.WriteString(w, "{}") // nolint
476
	})
ale's avatar
ale committed
477
478
479
480
	return &fakeKeyStore{
		Server: httptest.NewServer(h),
		values: values,
	}
481
482
483
}

func TestHTTP_LoginWithKeyStore(t *testing.T) {
ale's avatar
ale committed
484
	tmpdir, httpSrv, ks := startTestHTTPServerWithKeyStore(t)
485
486
487
488
489
490
491
492
493
494
495
	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")
496
	doGet(t, c, httpSrv.URL+"/?"+v.Encode(), checkStatusOk, checkLoginPageURL, checkLoginPasswordPage)
497
498
499
500
501
502

	// 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")
ale's avatar
ale committed
503
	doPostForm(t, c, httpSrv.URL+"/login", v, checkRedirectToTargetService)
ale's avatar
ale committed
504
505
506
507
508

	// Verify that the keystore has been called.
	if v := ks.values["testuser"]; v != "password" {
		t.Fatalf("keystore not called as expected: ks_values=%+v", ks.values)
	}
509
}
ale's avatar
ale committed
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524

func TestHTTP_CORS(t *testing.T) {
	tmpdir, httpSrv := startTestHTTPServer(t)
	defer os.RemoveAll(tmpdir)
	defer httpSrv.Close()

	c := newTestHTTPClient()

	// To test a CORS preflight request we have to login first.
	// 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")
525
	doGet(t, c, httpSrv.URL+"/?"+v.Encode(), checkStatusOk, checkLoginPageURL, checkLoginPasswordPage)
ale's avatar
ale committed
526
527
528
529
530
531

	// 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")
ale's avatar
ale committed
532
	doPostForm(t, c, httpSrv.URL+"/login", v, checkRedirectToTargetService)
ale's avatar
ale committed
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554

	// Simulate a CORS preflight request.
	v = make(url.Values)
	v.Set("s", "service.example.com/")
	v.Set("d", "https://service.example.com/admin/")
	v.Set("n", "averysecretnonce")
	req, err := http.NewRequest("OPTIONS", httpSrv.URL+"/?"+v.Encode(), nil)
	if err != nil {
		t.Fatalf("NewRequest(): %v", err)
	}
	req.Header.Set("Origin", "https://origin.example.com")
	req.Header.Set("Access-Control-Request-Method", "GET")
	resp, err := c.Do(req)
	if err != nil {
		t.Fatalf("http request error: %v", err)
	}
	defer resp.Body.Close()
	checkStatusOk(t, resp)
	if s := resp.Header.Get("Access-Control-Allow-Origin"); s != "https://origin.example.com" {
		t.Fatalf("Bad Access-Control-Allow-Origin returned to OPTIONS request: %s", s)
	}
}
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596

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