feat: endpoint to logout app subdomain URLs (#5428)

Co-authored-by: Bruno Quaresma <bruno@coder.com>
This commit is contained in:
Dean Sheather
2022-12-20 18:45:13 +00:00
committed by GitHub
co-authored by Bruno Quaresma
parent 86257ce7fc
commit 50dfc2082b
5 changed files with 391 additions and 33 deletions
+190
View File
@@ -21,6 +21,7 @@ import (
"cdr.dev/slog/sloggers/slogtest"
"github.com/coder/coder/agent"
"github.com/coder/coder/coderd"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/coderd/httpapi"
"github.com/coder/coder/coderd/httpmw"
@@ -892,6 +893,195 @@ func TestWorkspaceAppsProxySubdomain(t *testing.T) {
})
}
func TestAppSubdomainLogout(t *testing.T) {
t.Parallel()
keyID, keySecret, err := coderd.GenerateAPIKeyIDSecret()
require.NoError(t, err)
fakeAPIKey := fmt.Sprintf("%s-%s", keyID, keySecret)
cases := []struct {
name string
// The cookie to send with the request. The regular API key header
// is also sent to bypass any auth checks on this value, and to
// ensure that the logout code is safe when multiple keys are
// passed.
// Empty value means no cookie is sent, "-" means send a valid
// API key, and "bad-secret" means send a valid key ID with a bad
// secret.
cookie string
// You can use "access_url" to use the site access URL as the
// redirect URI, or "app_host" to use a valid app host.
redirectURI string
// If expectedStatus is not an error status, we expect the cookie to
// be deleted if it was set.
expectedStatus int
expectedBodyContains string
// If empty, the expected location is the redirectURI if the
// expected status code is http.StatusTemporaryRedirect (using the
// access URL if not set).
// You can use "access_url" to force the access URL.
expectedLocation string
}{
{
name: "OKAccessURL",
cookie: "-",
redirectURI: "access_url",
expectedStatus: http.StatusTemporaryRedirect,
},
{
name: "OKAppHost",
cookie: "-",
redirectURI: "app_host",
expectedStatus: http.StatusTemporaryRedirect,
},
{
name: "OKNoAPIKey",
cookie: "",
redirectURI: "access_url",
// Even if the devurl cookie is missing, we still redirect without
// any complaints.
expectedStatus: http.StatusTemporaryRedirect,
},
{
name: "OKBadAPIKey",
cookie: "test-api-key",
redirectURI: "access_url",
// Even if the devurl cookie is bad, we still delete the cookie and
// redirect without any complaints.
expectedStatus: http.StatusTemporaryRedirect,
},
{
name: "OKUnknownAPIKey",
cookie: fakeAPIKey,
redirectURI: "access_url",
expectedStatus: http.StatusTemporaryRedirect,
},
{
name: "BadAPIKeySecret",
cookie: "bad-secret",
redirectURI: "access_url",
expectedStatus: http.StatusUnauthorized,
expectedBodyContains: "API key secret is invalid",
},
{
name: "InvalidRedirectURI",
cookie: "-",
redirectURI: string([]byte{0x00}),
expectedStatus: http.StatusBadRequest,
expectedBodyContains: "Could not parse redirect URI",
},
{
name: "DisallowedRedirectURI",
cookie: "-",
redirectURI: "https://github.com/coder/coder",
// We don't allow redirecting to a different host, but we don't
// show an error page and just redirect to the access URL to avoid
// breaking the logout flow if the user is accessing from the wrong
// host.
expectedStatus: http.StatusTemporaryRedirect,
expectedLocation: "access_url",
},
}
for _, c := range cases {
c := c
t.Run(c.name, func(t *testing.T) {
t.Parallel()
client, _, _, _ := setupProxyTest(t)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
// The token should work.
_, err := client.User(ctx, codersdk.Me)
require.NoError(t, err)
appHost, err := client.GetAppHost(ctx)
require.NoError(t, err, "get app host")
if c.cookie == "-" {
c.cookie = client.SessionToken()
} else if c.cookie == "bad-secret" {
keyID, _, err := httpmw.SplitAPIToken(client.SessionToken())
require.NoError(t, err)
c.cookie = fmt.Sprintf("%s-%s", keyID, keySecret)
}
if c.redirectURI == "access_url" {
c.redirectURI = client.URL.String()
} else if c.redirectURI == "app_host" {
c.redirectURI = "http://" + strings.Replace(appHost.Host, "*", "something--something--something--something", 1) + "/"
}
if c.expectedLocation == "" && c.expectedStatus == http.StatusTemporaryRedirect {
c.expectedLocation = c.redirectURI
}
if c.expectedLocation == "access_url" {
c.expectedLocation = client.URL.String()
}
logoutURL := &url.URL{
Scheme: "http",
Host: strings.Replace(appHost.Host, "*", "coder-logout", 1),
Path: "/",
}
if c.redirectURI != "" {
q := logoutURL.Query()
q.Set("redirect_uri", c.redirectURI)
logoutURL.RawQuery = q.Encode()
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, logoutURL.String(), nil)
require.NoError(t, err, "create logout request")
// The header is prioritized over the devurl cookie if both are
// set, so this ensures we can trigger the logout code path with
// bad cookies during tests.
req.Header.Set(codersdk.SessionCustomHeader, client.SessionToken())
if c.cookie != "" {
req.AddCookie(&http.Cookie{
Name: httpmw.DevURLSessionTokenCookie,
Value: c.cookie,
})
}
client.HTTPClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
resp, err := client.HTTPClient.Do(req)
require.NoError(t, err, "do logout request")
defer resp.Body.Close()
require.Equal(t, c.expectedStatus, resp.StatusCode, "logout response status code")
if c.expectedStatus < 400 && c.cookie != "" {
cookies := resp.Cookies()
require.Len(t, cookies, 1, "logout response cookies")
cookie := cookies[0]
require.Equal(t, httpmw.DevURLSessionTokenCookie, cookie.Name)
require.Equal(t, "", cookie.Value)
require.True(t, cookie.Expires.Before(time.Now()), "cookie should be expired")
// The token shouldn't work anymore if it was the original valid
// session token.
if c.cookie == client.SessionToken() {
_, err = client.User(ctx, codersdk.Me)
require.Error(t, err)
}
}
if c.expectedBodyContains != "" {
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Contains(t, string(body), c.expectedBodyContains, "logout response body")
}
if c.expectedLocation != "" {
location := resp.Header.Get("Location")
require.Equal(t, c.expectedLocation, location, "logout response location")
}
})
}
}
func TestAppSharing(t *testing.T) {
t.Parallel()