diff --git a/integration/authorization_test.go b/integration/authorization_test.go index 4de1b80df..b55d5766c 100644 --- a/integration/authorization_test.go +++ b/integration/authorization_test.go @@ -16,116 +16,127 @@ func TestAuthorization(t *testing.T) { ctx, clearTimeout := context.WithTimeout(mainCtx, time.Second*30) defer clearTimeout() - t.Run("public", func(t *testing.T) { - client := testcluster.NewHTTPClient() + accessType := []string{"direct", "api"} + for _, at := range accessType { + t.Run(at, func(t *testing.T) { + var withAPI flows.AuthenticateOption - req, err := http.NewRequestWithContext(ctx, "GET", "https://httpdetails.localhost.pomerium.io", nil) - if err != nil { - t.Fatal(err) - } + if at == "api" { + withAPI = flows.WithAPI() + } - res, err := client.Do(req) - if !assert.NoError(t, err, "unexpected http error") { - return - } - defer res.Body.Close() + t.Run("public", func(t *testing.T) { + client := testcluster.NewHTTPClient() - assert.Equal(t, http.StatusOK, res.StatusCode, "unexpected status code, headers=%v", res.Header) - }) - t.Run("domains", func(t *testing.T) { - t.Run("allowed", func(t *testing.T) { - client := testcluster.NewHTTPClient() - res, err := flows.Authenticate(ctx, client, mustParseURL("https://httpdetails.localhost.pomerium.io/by-domain"), - flows.WithEmail("bob@dogs.test"), flows.WithGroups("user")) - if assert.NoError(t, err) { + req, err := http.NewRequestWithContext(ctx, "GET", "https://httpdetails.localhost.pomerium.io", nil) + if err != nil { + t.Fatal(err) + } + + res, err := client.Do(req) + if !assert.NoError(t, err, "unexpected http error") { + return + } + defer res.Body.Close() + + assert.Equal(t, http.StatusOK, res.StatusCode, "unexpected status code, headers=%v", res.Header) + }) + t.Run("domains", func(t *testing.T) { + t.Run("allowed", func(t *testing.T) { + client := testcluster.NewHTTPClient() + res, err := flows.Authenticate(ctx, client, mustParseURL("https://httpdetails.localhost.pomerium.io/by-domain"), + withAPI, flows.WithEmail("bob@dogs.test"), flows.WithGroups("user")) + if assert.NoError(t, err) { + assert.Equal(t, http.StatusOK, res.StatusCode, "expected OK for dogs.test") + } + }) + t.Run("not allowed", func(t *testing.T) { + client := testcluster.NewHTTPClient() + res, err := flows.Authenticate(ctx, client, mustParseURL("https://httpdetails.localhost.pomerium.io/by-domain"), + withAPI, flows.WithEmail("joe@cats.test"), flows.WithGroups("user")) + if assert.NoError(t, err) { + assertDeniedAccess(t, res, "expected Forbidden for cats.test") + } + }) + }) + t.Run("users", func(t *testing.T) { + t.Run("allowed", func(t *testing.T) { + client := testcluster.NewHTTPClient() + res, err := flows.Authenticate(ctx, client, mustParseURL("https://httpdetails.localhost.pomerium.io/by-user"), + withAPI, flows.WithEmail("bob@dogs.test"), flows.WithGroups("user")) + if assert.NoError(t, err) { + assert.Equal(t, http.StatusOK, res.StatusCode, "expected OK for bob@dogs.test") + } + }) + t.Run("not allowed", func(t *testing.T) { + client := testcluster.NewHTTPClient() + res, err := flows.Authenticate(ctx, client, mustParseURL("https://httpdetails.localhost.pomerium.io/by-user"), + withAPI, flows.WithEmail("joe@cats.test"), flows.WithGroups("user")) + if assert.NoError(t, err) { + assertDeniedAccess(t, res, "expected Forbidden for joe@cats.test") + } + }) + }) + t.Run("groups", func(t *testing.T) { + t.Run("allowed", func(t *testing.T) { + client := testcluster.NewHTTPClient() + res, err := flows.Authenticate(ctx, client, mustParseURL("https://httpdetails.localhost.pomerium.io/by-group"), + withAPI, flows.WithEmail("bob@dogs.test"), flows.WithGroups("admin", "user")) + if assert.NoError(t, err) { + assert.Equal(t, http.StatusOK, res.StatusCode, "expected OK for admin") + } + }) + t.Run("not allowed", func(t *testing.T) { + client := testcluster.NewHTTPClient() + res, err := flows.Authenticate(ctx, client, mustParseURL("https://httpdetails.localhost.pomerium.io/by-group"), + withAPI, flows.WithEmail("joe@cats.test"), flows.WithGroups("user")) + if assert.NoError(t, err) { + assertDeniedAccess(t, res, "expected Forbidden for user, but got %d", res.StatusCode) + } + }) + }) + + t.Run("refresh", func(t *testing.T) { + client := testcluster.NewHTTPClient() + res, err := flows.Authenticate(ctx, client, mustParseURL("https://httpdetails.localhost.pomerium.io/by-domain"), + withAPI, flows.WithEmail("bob@dogs.test"), flows.WithGroups("user"), flows.WithTokenExpiration(time.Second)) + if !assert.NoError(t, err) { + return + } assert.Equal(t, http.StatusOK, res.StatusCode, "expected OK for dogs.test") - } - }) - t.Run("not allowed", func(t *testing.T) { - client := testcluster.NewHTTPClient() - res, err := flows.Authenticate(ctx, client, mustParseURL("https://httpdetails.localhost.pomerium.io/by-domain"), - flows.WithEmail("joe@cats.test"), flows.WithGroups("user")) - if assert.NoError(t, err) { - assertDeniedAccess(t, res, "expected Forbidden for cats.test") - } - }) - }) - t.Run("users", func(t *testing.T) { - t.Run("allowed", func(t *testing.T) { - client := testcluster.NewHTTPClient() - res, err := flows.Authenticate(ctx, client, mustParseURL("https://httpdetails.localhost.pomerium.io/by-user"), - flows.WithEmail("bob@dogs.test"), flows.WithGroups("user")) - if assert.NoError(t, err) { - assert.Equal(t, http.StatusOK, res.StatusCode, "expected OK for bob@dogs.test") - } - }) - t.Run("not allowed", func(t *testing.T) { - client := testcluster.NewHTTPClient() - res, err := flows.Authenticate(ctx, client, mustParseURL("https://httpdetails.localhost.pomerium.io/by-user"), - flows.WithEmail("joe@cats.test"), flows.WithGroups("user")) - if assert.NoError(t, err) { - assertDeniedAccess(t, res, "expected Forbidden for joe@cats.test") - } - }) - }) - t.Run("groups", func(t *testing.T) { - t.Run("allowed", func(t *testing.T) { - client := testcluster.NewHTTPClient() - res, err := flows.Authenticate(ctx, client, mustParseURL("https://httpdetails.localhost.pomerium.io/by-group"), - flows.WithEmail("bob@dogs.test"), flows.WithGroups("admin", "user")) - if assert.NoError(t, err) { - assert.Equal(t, http.StatusOK, res.StatusCode, "expected OK for admin") - } - }) - t.Run("not allowed", func(t *testing.T) { - client := testcluster.NewHTTPClient() - res, err := flows.Authenticate(ctx, client, mustParseURL("https://httpdetails.localhost.pomerium.io/by-group"), - flows.WithEmail("joe@cats.test"), flows.WithGroups("user")) - if assert.NoError(t, err) { - assertDeniedAccess(t, res, "expected Forbidden for user, but got %d", res.StatusCode) - } - }) - }) + res.Body.Close() - t.Run("refresh", func(t *testing.T) { - client := testcluster.NewHTTPClient() - res, err := flows.Authenticate(ctx, client, mustParseURL("https://httpdetails.localhost.pomerium.io/by-domain"), - flows.WithEmail("bob@dogs.test"), flows.WithGroups("user"), flows.WithTokenExpiration(time.Second)) - if !assert.NoError(t, err) { - return - } - assert.Equal(t, http.StatusOK, res.StatusCode, "expected OK for dogs.test") - res.Body.Close() + // poll till we get a new cookie because of a refreshed session + ticker := time.NewTicker(time.Millisecond * 500) + defer ticker.Stop() + deadline := time.NewTimer(time.Second * 10) + defer deadline.Stop() + for i := 0; ; i++ { + select { + case <-ticker.C: + case <-deadline.C: + t.Fatal("timed out waiting for refreshed session") + return + case <-ctx.Done(): + t.Fatal("timed out waiting for refreshed session") + return + } - // poll till we get a new cookie because of a refreshed session - ticker := time.NewTicker(time.Millisecond * 500) - defer ticker.Stop() - deadline := time.NewTimer(time.Second * 10) - defer deadline.Stop() - for i := 0; ; i++ { - select { - case <-ticker.C: - case <-deadline.C: - t.Fatal("timed out waiting for refreshed session") - return - case <-ctx.Done(): - t.Fatal("timed out waiting for refreshed session") - return - } - - res, err = client.Get(mustParseURL("https://httpdetails.localhost.pomerium.io/by-domain").String()) - if !assert.NoError(t, err) { - return - } - res.Body.Close() - if !assert.Equal(t, http.StatusOK, res.StatusCode, "failed after %d times", i+1) { - return - } - if res.Header.Get("Set-Cookie") != "" { - break - } - } - }) + res, err = client.Get(mustParseURL("https://httpdetails.localhost.pomerium.io/by-domain").String()) + if !assert.NoError(t, err) { + return + } + res.Body.Close() + if !assert.Equal(t, http.StatusOK, res.StatusCode, "failed after %d times", i+1) { + return + } + if res.Header.Get("Set-Cookie") != "" { + break + } + } + }) + }) + } } func mustParseURL(str string) *url.URL { diff --git a/integration/internal/flows/flows.go b/integration/internal/flows/flows.go index 9f8d9d097..dbe1809f0 100644 --- a/integration/internal/flows/flows.go +++ b/integration/internal/flows/flows.go @@ -4,6 +4,7 @@ package flows import ( "context" "fmt" + "io/ioutil" "net/http" "net/url" "strconv" @@ -11,18 +12,21 @@ import ( "time" "github.com/pomerium/pomerium/integration/internal/forms" + "github.com/pomerium/pomerium/internal/urlutil" ) const ( authenticateHostname = "authenticate.localhost.pomerium.io" openidHostname = "openid.localhost.pomerium.io" pomeriumCallbackPath = "/.pomerium/callback/" + pomeriumAPIPath = "/.pomerium/api/v1/login" ) type authenticateConfig struct { email string groups []string tokenExpiration time.Duration + apiPath string } // An AuthenticateOption is an option for authentication. @@ -33,7 +37,9 @@ func getAuthenticateConfig(options ...AuthenticateOption) *authenticateConfig { tokenExpiration: time.Hour * 24, } for _, option := range options { - option(cfg) + if option != nil { + option(cfg) + } } return cfg } @@ -59,11 +65,46 @@ func WithTokenExpiration(tokenExpiration time.Duration) AuthenticateOption { } } +// WithAPI tells authentication to use API authentication flow. +func WithAPI() AuthenticateOption { + return func(cfg *authenticateConfig) { + cfg.apiPath = pomeriumAPIPath + } +} + // Authenticate submits a request to a URL, expects a redirect to authenticate and then openid and logs in. // Finally it expects to redirect back to the original page. func Authenticate(ctx context.Context, client *http.Client, url *url.URL, options ...AuthenticateOption) (*http.Response, error) { cfg := getAuthenticateConfig(options...) originalHostname := url.Hostname() + var err error + + if cfg.apiPath != "" { + apiLogin := url + q := apiLogin.Query() + q.Set(urlutil.QueryRedirectURI, url.String()) + apiLogin.RawQuery = q.Encode() + + apiLogin.Path = cfg.apiPath + req, err := http.NewRequestWithContext(ctx, "GET", apiLogin.String(), nil) + req.Header.Set("Accept", "application/json") + if err != nil { + return nil, err + } + res, err := client.Do(req) + if err != nil { + return nil, err + } + bodyBytes, err := ioutil.ReadAll(res.Body) + if err != nil { + return nil, err + } + defer res.Body.Close() + url, err = url.Parse(string(bodyBytes)) + if err != nil { + return nil, err + } + } req, err := http.NewRequestWithContext(ctx, "GET", url.String(), nil) if err != nil {