mirror of
https://github.com/coder/coder.git
synced 2026-06-03 04:58:23 +00:00
fcf431c1d7
This PR fixes a workspace app authentication bug where requests that include an `Authorization` header (intended for the upstream app) can cause Coder to ignore the workspace app session cookie (`coder_subdomain_app_session_token_*` / `coder_path_app_session_token`). When that happens, Coder fails to mint or renew `coder_signed_app_token` and redirects to `/api/v2/applications/auth-redirect` instead of proxying the request to the workspace. This commonly shows up when users run a frontend and backend in the same workspace and the backend requires `Authorization` (for example, `curl -H "Authorization: bearer ..."` or browser `fetch()` calls). Related issues / context: * Primary bug report and repro: [https://github.com/coder/coder/issues/21467](https://github.com/coder/coder/issues/21467) * Related symptoms reported as CORS / redirect failures for workspace apps: * [https://github.com/coder/coder/issues/20667](https://github.com/coder/coder/issues/20667) * [https://github.com/coder/coder/issues/19728](https://github.com/coder/coder/issues/19728) ## Root Cause In `coderd/workspaceapps/cookies.go`, `AppCookies.TokenFromRequest` checked `httpmw.APITokenFromRequest(r)` first. That helper returns a token from several places, including `Authorization: Bearer ...`. As a result, when a request included an upstream `Authorization` header, that header value was returned as the “session token” for the app proxy, and `coder_subdomain_app_session_token_*` was never read. Authentication then failed and the request was treated as signed out. ## Fix Change the precedence in `AppCookies.TokenFromRequest`: 1. First check the access-method-specific cookie: * subdomain apps: `coder_subdomain_app_session_token_{hash}` * path apps: `coder_path_app_session_token` 2. If not present, fall back to `httpmw.APITokenFromRequest(r)` (so non-browser clients can still authenticate via query, header, or bearer tokens if they really want to). This ensures that: * Backend requests that require `Authorization` still reach the workspace. * `coder_signed_app_token` can be renewed from the app session cookie even when `Authorization` is present. * `Authorization` is still forwarded to the upstream app (the reverse proxy code does not strip it). Initially, I attempted workarounds ([https://github.com/coder/coder/issues/20667#issuecomment-3868578388](https://github.com/coder/coder/issues/20667#issuecomment-3868578388), [https://github.com/coder/coder/issues/19728#issuecomment-3868578093](https://github.com/coder/coder/issues/19728#issuecomment-3868578093)), but adding `/auth-redirect` to the permissive CORS paths and extending the validity of workspace app auth tokens from 1 minute to 1 hour only partially masked the issue. After workspace restarts and token expiry, I no longer saw CORS errors, but the tokens were still not renewed. After patching my local Nix-based setup on Coder v1.30.0 with this change, I can no longer observe this behavior.
53 lines
1.9 KiB
Go
53 lines
1.9 KiB
Go
package workspaceapps_test
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/coder/coder/v2/coderd/workspaceapps"
|
|
"github.com/coder/coder/v2/codersdk"
|
|
)
|
|
|
|
func TestAppCookies(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const (
|
|
domain = "example.com"
|
|
hash = "a379a6f6eeafb9a55e378c118034e275"
|
|
expectedSubdomainCookie = codersdk.SubdomainAppSessionTokenCookie + "_" + hash
|
|
)
|
|
|
|
cookies := workspaceapps.NewAppCookies(domain)
|
|
require.Equal(t, codersdk.PathAppSessionTokenCookie, cookies.PathAppSessionToken)
|
|
require.Equal(t, expectedSubdomainCookie, cookies.SubdomainAppSessionToken)
|
|
require.Equal(t, codersdk.SignedAppTokenCookie, cookies.SignedAppToken)
|
|
|
|
require.Equal(t, cookies.PathAppSessionToken, cookies.CookieNameForAccessMethod(workspaceapps.AccessMethodPath))
|
|
require.Equal(t, cookies.PathAppSessionToken, cookies.CookieNameForAccessMethod(workspaceapps.AccessMethodTerminal))
|
|
require.Equal(t, cookies.SubdomainAppSessionToken, cookies.CookieNameForAccessMethod(workspaceapps.AccessMethodSubdomain))
|
|
|
|
// A new cookies object with a different domain should have a different
|
|
// subdomain cookie.
|
|
newCookies := workspaceapps.NewAppCookies("different.com")
|
|
require.NotEqual(t, cookies.SubdomainAppSessionToken, newCookies.SubdomainAppSessionToken)
|
|
}
|
|
|
|
func TestAppCookies_TokenFromRequest_PrefersAppCookieOverAuthorizationBearer(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cookies := workspaceapps.NewAppCookies("apps.example.com")
|
|
|
|
req := httptest.NewRequest("GET", "https://8081--agent--workspace--user.apps.example.com/", nil)
|
|
req.Header.Set("Authorization", "Bearer whatever")
|
|
req.AddCookie(&http.Cookie{
|
|
Name: cookies.CookieNameForAccessMethod(workspaceapps.AccessMethodSubdomain),
|
|
Value: "subdomain-session-token",
|
|
})
|
|
|
|
got := cookies.TokenFromRequest(req, workspaceapps.AccessMethodSubdomain)
|
|
require.Equal(t, "subdomain-session-token", got)
|
|
}
|