mirror of
https://github.com/coder/coder.git
synced 2026-06-03 13:08:25 +00:00
42dd544d90
There is currently an issue with subdomain workspace apps on workspace
proxies, where if you have a workspace proxy wildcard nested beneath the
primary wildcard, cookies from the primary may be sent to the server
before cookies from the proxy specifically.
Currently:
1. Use a subdomain app via the primary proxy `*.coder.corp.com`
a. Client sends no cookies
a. Server does token smuggling flow
a. Server sets a cookie `coder_subdomain_app_session_token` on
`*.coder.corp.com`
a. Server redirects client to reload the page
a. Request should succeed as usual
1. Wait until the primary proxy's session token cookie has expired in
the database (or make it invalid yourself)
1. Use a subdomain app via a separate proxy `*.sydney.coder.corp.com`
a. Client sends `coder_subdomain_app_session_token` cookie from
`*.coder.corp.com`
a. Server validates supplied cookie, it fails because it's expired
a. Server does token smuggling flow
a. Server sets a cookie `coder_subdomain_app_session_token` on
`*.sydney.coder.corp.com`
a. Server redirects client to reload page
a. Client sends BOTH cookies.
a. The server will only process the first cookie it receives, so if the
expired cookie for the primary proxy is sent first the request will end
up in a permanent loop on step b.
The fix is to append `_{hash(wildcard_access_url)}` to the subdomain
cookies as we cannot control browser behavior further. This avoids the
conflict as each proxy will only read it's specific cookie.
107 lines
3.5 KiB
Go
107 lines
3.5 KiB
Go
package workspaceapps
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/url"
|
|
"time"
|
|
|
|
"cdr.dev/slog"
|
|
"github.com/coder/coder/v2/codersdk"
|
|
)
|
|
|
|
const (
|
|
// TODO(@deansheather): configurable expiry
|
|
DefaultTokenExpiry = time.Minute
|
|
|
|
// RedirectURIQueryParam is the query param for the app URL to be passed
|
|
// back to the API auth endpoint on the main access URL.
|
|
RedirectURIQueryParam = "redirect_uri"
|
|
)
|
|
|
|
type ResolveRequestOptions struct {
|
|
Logger slog.Logger
|
|
SignedTokenProvider SignedTokenProvider
|
|
Cookies AppCookies
|
|
CookieCfg codersdk.HTTPCookieConfig
|
|
|
|
DashboardURL *url.URL
|
|
PathAppBaseURL *url.URL
|
|
AppHostname string
|
|
|
|
AppRequest Request
|
|
// TODO: Replace these 2 fields with a "BrowserURL" field which is used for
|
|
// redirecting the user back to their initial request after authenticating.
|
|
// AppPath is the path under the app that was hit.
|
|
AppPath string
|
|
// AppQuery is the raw query of the request.
|
|
AppQuery string
|
|
}
|
|
|
|
func ResolveRequest(rw http.ResponseWriter, r *http.Request, opts ResolveRequestOptions) (*SignedToken, bool) {
|
|
appReq := opts.AppRequest.Normalize()
|
|
err := appReq.Check()
|
|
if err != nil {
|
|
// This is a 500 since it's a coder server or proxy that's making this
|
|
// request struct based on details from the request. The values should
|
|
// already be validated before they are put into the struct.
|
|
WriteWorkspaceApp500(opts.Logger, opts.DashboardURL, rw, r, &appReq, err, "invalid app request")
|
|
return nil, false
|
|
}
|
|
|
|
token, ok := opts.SignedTokenProvider.FromRequest(r)
|
|
if ok && token.MatchesRequest(appReq) {
|
|
// The request has a valid signed app token and it matches the request.
|
|
return token, true
|
|
}
|
|
|
|
issueReq := IssueTokenRequest{
|
|
AppRequest: appReq,
|
|
PathAppBaseURL: opts.PathAppBaseURL.String(),
|
|
AppHostname: opts.AppHostname,
|
|
SessionToken: opts.Cookies.TokenFromRequest(r, appReq.AccessMethod),
|
|
AppPath: opts.AppPath,
|
|
AppQuery: opts.AppQuery,
|
|
}
|
|
|
|
token, tokenStr, ok := opts.SignedTokenProvider.Issue(r.Context(), rw, r, issueReq)
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
|
|
// Write the signed app token cookie.
|
|
//
|
|
// For path apps, this applies to only the path app base URL on the current
|
|
// domain, e.g.
|
|
// /@user/workspace[.agent]/apps/path-app/
|
|
//
|
|
// For subdomain apps, this applies to the entire subdomain, e.g.
|
|
// app--agent--workspace--user.apps.example.com
|
|
http.SetCookie(rw, opts.CookieCfg.Apply(&http.Cookie{
|
|
Name: codersdk.SignedAppTokenCookie,
|
|
Value: tokenStr,
|
|
Path: appReq.BasePath,
|
|
HttpOnly: true,
|
|
Expires: token.Expiry.Time(),
|
|
}))
|
|
|
|
return token, true
|
|
}
|
|
|
|
// SignedTokenProvider provides signed workspace app tokens (aka. app tickets).
|
|
type SignedTokenProvider interface {
|
|
// FromRequest returns a parsed token from the request. If the request does
|
|
// not contain a signed app token or is is invalid (expired, invalid
|
|
// signature, etc.), it returns false.
|
|
FromRequest(r *http.Request) (*SignedToken, bool)
|
|
// Issue mints a new token for the given app request. It uses the long-lived
|
|
// session token in the HTTP request to authenticate and authorize the
|
|
// client for the given workspace app. The token is returned in struct and
|
|
// string form. The string form should be written as a cookie.
|
|
//
|
|
// If the request is invalid or the user is not authorized to access the
|
|
// app, false is returned. An error page is written to the response writer
|
|
// in this case.
|
|
Issue(ctx context.Context, rw http.ResponseWriter, r *http.Request, appReq IssueTokenRequest) (*SignedToken, string, bool)
|
|
}
|