package server import ( "context" "crypto/tls" "encoding/json" "errors" "io" "io/ioutil" "net/http" "net/http/cookiejar" "net/http/httptest" "net/url" "os" "regexp" "strings" "testing" "git.autistici.org/id/auth" "git.autistici.org/id/keystore" ) type fakeAuthClient struct{} func (c *fakeAuthClient) Authenticate(_ context.Context, req *auth.Request) (*auth.Response, error) { p := string(req.Password) info := &auth.UserInfo{Shard: "shard1"} switch { case req.Username == "testuser" && p == "password": return &auth.Response{Status: auth.StatusOK, UserInfo: info}, nil case req.Username == "test2fa" && p == "password" && req.OTP == "123456": return &auth.Response{Status: auth.StatusOK, UserInfo: info}, nil case req.Username == "test2fa" && p == "password": return &auth.Response{ Status: auth.StatusInsufficientCredentials, TFAMethods: []auth.TFAMethod{auth.TFAMethodOTP}, }, nil } return &auth.Response{Status: auth.StatusError}, nil } func createTestHTTPServer(t testing.TB, config *Config) *httptest.Server { svc, err := NewLoginService(config) if err != nil { t.Fatal("NewLoginService():", err) } srv, err := New(svc, &fakeAuthClient{}, config) if err != nil { t.Fatal("New():", err) } return httptest.NewTLSServer(srv.Handler()) } func startTestHTTPServer(t testing.TB) (string, *httptest.Server) { tmpdir, _ := ioutil.TempDir("", "") config := testConfig(t, tmpdir, "") return tmpdir, createTestHTTPServer(t, config) } func startTestHTTPServerWithKeyStore(t testing.TB) (string, *httptest.Server) { ks := createFakeKeyStore(t, "testuser", "password") tmpdir, _ := ioutil.TempDir("", "") config := testConfig(t, tmpdir, ks.URL) return tmpdir, createTestHTTPServer(t, config) } func newTestHTTPClient() *http.Client { jar, _ := cookiejar.New(nil) transport := NewLoggedTransport(&http.Transport{ TLSClientConfig: &tls.Config{ InsecureSkipVerify: true, }, }, 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") } if !strings.HasPrefix(req.URL.Host, "127.0.0.1:") { return http.ErrUseLastResponse } return nil }, } } func TestHTTP_ServeStaticAsset(t *testing.T) { tmpdir, httpSrv := startTestHTTPServer(t) defer os.RemoveAll(tmpdir) defer httpSrv.Close() c := newTestHTTPClient() resp, err := c.Get(httpSrv.URL + "/static/js/u2f.js") if err != nil { t.Fatal("http.Get():", err) } if resp.StatusCode != 200 { t.Fatalf("bad status: %s", resp.Status) } } 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) if err != nil { t.Fatalf("http.Get(%s): %v", relativeURL, err) } defer resp.Body.Close() for _, f := range checkResponse { f(t, resp) } } 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) if err != nil { t.Fatalf("http.Get(%s): %v", relativeURL, err) } 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) } } 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")) } } var usernameFieldRx = regexp.MustCompile(`<input[^>]*name="username"`) func checkLoginPasswordPage(t testing.TB, resp *http.Response) { if resp.Request.URL.Path != "/login" { 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 !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) { if resp.Request.URL.Path != "/login/otp" { 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)) } } func TestHTTP_Login(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, httpSrv, c, "/?"+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) } 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") doGet(t, httpSrv, c, "/?"+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) // 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) } 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") doGet(t, httpSrv, c, "/?"+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) // Make a logout request. doGet(t, httpSrv, c, "/logout", checkStatusOk) // 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) } 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") doGet(t, httpSrv, c, "/?"+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) // 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) } func createFakeKeyStore(t testing.TB, username, password string) *httptest.Server { 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 } if openReq.Username != username { t.Errorf("bad username in keystore Open request: expected %s, got %s", username, openReq.Username) } if openReq.Password != password { t.Errorf("bad password in keystore Open request: expected %s, got %s", password, openReq.Password) } w.Header().Set("Content-Type", "application/json") io.WriteString(w, "{}") // nolint }) return httptest.NewServer(h) } func TestHTTP_LoginWithKeyStore(t *testing.T) { tmpdir, httpSrv := startTestHTTPServerWithKeyStore(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, httpSrv, c, "/?"+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) } 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") doGet(t, httpSrv, c, "/?"+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) // 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) } }