Skip to content
Snippets Groups Projects
Commit dfe0056b authored by ale's avatar ale
Browse files

Add an integration test

parent c2e0f43c
Branches
No related tags found
1 merge request!6Refactor the login handler
Pipeline #5388 passed
......@@ -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)
......
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)
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment