Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 020b4b5

Browse files
committed
Add proxy token provider
1 parent 2aebe77 commit 020b4b5

File tree

11 files changed

+189
-49
lines changed

11 files changed

+189
-49
lines changed

coderd/coderd.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -338,12 +338,14 @@ func New(options *Options) *API {
338338
AccessURL: api.AccessURL,
339339
Hostname: api.AppHostname,
340340
HostnameRegex: api.AppHostnameRegex,
341-
DeploymentValues: options.DeploymentValues,
342341
RealIPConfig: options.RealIPConfig,
343342

344343
SignedTokenProvider: api.WorkspaceAppsProvider,
345344
WorkspaceConnCache: api.workspaceAgentCache,
346345
AppSecurityKey: options.AppSecurityKey,
346+
347+
DisablePathApps: options.DeploymentValues.DisablePathApps.Value(),
348+
SecureAuthCookie: options.DeploymentValues.SecureAuthCookie.Value(),
347349
}
348350

349351
apiKeyMiddleware := httpmw.ExtractAPIKeyMW(httpmw.ExtractAPIKeyConfig{

coderd/httpmw/apikey.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ func ExtractAPIKey(rw http.ResponseWriter, r *http.Request, cfg ExtractAPIKeyCon
167167
return nil, nil, false
168168
}
169169

170-
token := apiTokenFromRequest(r)
170+
token := ApiTokenFromRequest(r)
171171
if token == "" {
172172
return optionalWrite(http.StatusUnauthorized, codersdk.Response{
173173
Message: SignedOutErrorMessage,
@@ -376,14 +376,14 @@ func ExtractAPIKey(rw http.ResponseWriter, r *http.Request, cfg ExtractAPIKeyCon
376376
return &key, &authz, true
377377
}
378378

379-
// apiTokenFromRequest returns the api token from the request.
379+
// ApiTokenFromRequest returns the api token from the request.
380380
// Find the session token from:
381381
// 1: The cookie
382382
// 1: The devurl cookie
383383
// 3: The old cookie
384384
// 4. The coder_session_token query parameter
385385
// 5. The custom auth header
386-
func apiTokenFromRequest(r *http.Request) string {
386+
func ApiTokenFromRequest(r *http.Request) string {
387387
cookie, err := r.Cookie(codersdk.SessionTokenCookie)
388388
if err == nil && cookie.Value != "" {
389389
return cookie.Value

coderd/httpmw/workspaceagent.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ func ExtractWorkspaceAgent(db database.Store) func(http.Handler) http.Handler {
3232
return func(next http.Handler) http.Handler {
3333
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
3434
ctx := r.Context()
35-
tokenValue := apiTokenFromRequest(r)
35+
tokenValue := ApiTokenFromRequest(r)
3636
if tokenValue == "" {
3737
httpapi.Write(ctx, rw, http.StatusUnauthorized, codersdk.Response{
3838
Message: fmt.Sprintf("Cookie %q must be provided.", codersdk.SessionTokenCookie),

coderd/workspaceapps/db.go

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -55,23 +55,7 @@ func NewDBTokenProvider(log slog.Logger, accessURL *url.URL, authz rbac.Authoriz
5555
}
5656

5757
func (p *DBTokenProvider) TokenFromRequest(r *http.Request) (*SignedToken, bool) {
58-
// Get the existing token from the request.
59-
tokenCookie, err := r.Cookie(codersdk.DevURLSignedAppTokenCookie)
60-
if err == nil {
61-
token, err := p.SigningKey.VerifySignedToken(tokenCookie.Value)
62-
if err == nil {
63-
req := token.Request.Normalize()
64-
err := req.Validate()
65-
if err == nil {
66-
// The request has a valid signed app token, which is a valid
67-
// token signed by us. The caller must check that it matches
68-
// the request.
69-
return &token, true
70-
}
71-
}
72-
}
73-
74-
return nil, false
58+
return TokenFromRequest(r, p.SigningKey)
7559
}
7660

7761
// ResolveRequest takes an app request, checks if it's valid and authenticated,

coderd/workspaceapps/proxy.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,14 +78,16 @@ type Server struct {
7878
Hostname string
7979
// HostnameRegex contains the regex version of Hostname as generated by
8080
// httpapi.CompileHostnamePattern(). It MUST be set if Hostname is set.
81-
HostnameRegex *regexp.Regexp
82-
DeploymentValues *codersdk.DeploymentValues
83-
RealIPConfig *httpmw.RealIPConfig
81+
HostnameRegex *regexp.Regexp
82+
RealIPConfig *httpmw.RealIPConfig
8483

8584
SignedTokenProvider SignedTokenProvider
8685
WorkspaceConnCache *wsconncache.Cache
8786
AppSecurityKey SecurityKey
8887

88+
DisablePathApps bool
89+
SecureAuthCookie bool
90+
8991
websocketWaitMutex sync.Mutex
9092
websocketWaitGroup sync.WaitGroup
9193
}
@@ -120,7 +122,7 @@ func (s *Server) Attach(r chi.Router) {
120122
// workspaceAppsProxyPath proxies requests to a workspace application
121123
// through a relative URL path.
122124
func (s *Server) workspaceAppsProxyPath(rw http.ResponseWriter, r *http.Request) {
123-
if s.DeploymentValues.DisablePathApps.Value() {
125+
if s.DisablePathApps {
124126
site.RenderStaticErrorPage(rw, r, site.ErrorPageData{
125127
Status: http.StatusUnauthorized,
126128
Title: "Unauthorized",
@@ -385,7 +387,7 @@ func (s *Server) setWorkspaceAppCookie(rw http.ResponseWriter, r *http.Request,
385387
MaxAge: 0,
386388
HttpOnly: true,
387389
SameSite: http.SameSiteLaxMode,
388-
Secure: s.DeploymentValues.SecureAuthCookie.Value(),
390+
Secure: s.SecureAuthCookie,
389391
})
390392

391393
return true

coderd/workspaceapps/token.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@ import (
44
"encoding/base64"
55
"encoding/hex"
66
"encoding/json"
7+
"net/http"
78
"time"
89

910
"github.com/go-jose/go-jose/v3"
1011
"github.com/google/uuid"
1112
"golang.org/x/xerrors"
1213

1314
"github.com/coder/coder/coderd/database"
15+
"github.com/coder/coder/codersdk"
1416
)
1517

1618
const (
@@ -217,3 +219,23 @@ func (k SecurityKey) DecryptAPIKey(encryptedAPIKey string) (string, error) {
217219

218220
return payload.APIKey, nil
219221
}
222+
223+
func TokenFromRequest(r *http.Request, key SecurityKey) (*SignedToken, bool) {
224+
// Get the existing token from the request.
225+
tokenCookie, err := r.Cookie(codersdk.DevURLSignedAppTokenCookie)
226+
if err == nil {
227+
token, err := key.VerifySignedToken(tokenCookie.Value)
228+
if err == nil {
229+
req := token.Request.Normalize()
230+
err := req.Validate()
231+
if err == nil {
232+
// The request has a valid signed app token, which is a valid
233+
// token signed by us. The caller must check that it matches
234+
// the request.
235+
return &token, true
236+
}
237+
}
238+
}
239+
240+
return nil, false
241+
}

enterprise/coderd/workspaceproxy_test.go

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
package coderd_test
22

33
import (
4+
"net/http/httptest"
45
"testing"
56

67
"github.com/google/uuid"
78
"github.com/moby/moby/pkg/namesgenerator"
89
"github.com/stretchr/testify/require"
910

11+
"cdr.dev/slog"
12+
"cdr.dev/slog/sloggers/slogtest"
13+
"github.com/coder/coder/agent"
1014
"github.com/coder/coder/coderd/coderdtest"
1115
"github.com/coder/coder/coderd/database/dbtestutil"
1216
"github.com/coder/coder/coderd/workspaceapps"
1317
"github.com/coder/coder/codersdk"
18+
"github.com/coder/coder/codersdk/agentsdk"
1419
"github.com/coder/coder/enterprise/coderd/coderdenttest"
1520
"github.com/coder/coder/enterprise/coderd/license"
1621
"github.com/coder/coder/enterprise/wsproxy/wsproxysdk"
@@ -92,7 +97,21 @@ func TestIssueSignedAppToken(t *testing.T) {
9297
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
9398
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
9499
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
95-
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
100+
build := coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
101+
workspace.LatestBuild = build
102+
103+
// Connect an agent to the workspace
104+
agentClient := agentsdk.New(client.URL)
105+
agentClient.SetSessionToken(authToken)
106+
agentCloser := agent.New(agent.Options{
107+
Client: agentClient,
108+
Logger: slogtest.Make(t, nil).Named("agent").Leveled(slog.LevelDebug),
109+
})
110+
defer func() {
111+
_ = agentCloser.Close()
112+
}()
113+
114+
coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)
96115

97116
ctx := testutil.Context(t, testutil.WaitLong)
98117
proxyRes, err := client.CreateWorkspaceProxy(ctx, codersdk.CreateWorkspaceProxyRequest{
@@ -119,16 +138,23 @@ func TestIssueSignedAppToken(t *testing.T) {
119138
require.Error(t, err)
120139
})
121140

141+
goodRequest := wsproxysdk.IssueSignedAppTokenRequest{
142+
AppRequest: workspaceapps.Request{
143+
BasePath: "/app",
144+
AccessMethod: workspaceapps.AccessMethodTerminal,
145+
WorkspaceAndAgent: workspace.ID.String(),
146+
AgentNameOrID: build.Resources[0].Agents[0].ID.String(),
147+
},
148+
SessionToken: client.SessionToken(),
149+
}
122150
t.Run("OK", func(t *testing.T) {
123-
_, err = proxyClient.IssueSignedAppToken(ctx, wsproxysdk.IssueSignedAppTokenRequest{
124-
AppRequest: workspaceapps.Request{
125-
BasePath: "/app",
126-
AccessMethod: workspaceapps.AccessMethodTerminal,
127-
UsernameOrID: user.UserID.String(),
128-
WorkspaceAndAgent: workspace.ID.String(),
129-
},
130-
SessionToken: client.SessionToken(),
131-
})
151+
_, err = proxyClient.IssueSignedAppToken(ctx, goodRequest)
132152
require.NoError(t, err)
133153
})
154+
155+
t.Run("OKHTML", func(t *testing.T) {
156+
rw := httptest.NewRecorder()
157+
_, ok := proxyClient.IssueSignedAppTokenHTML(ctx, rw, goodRequest)
158+
require.True(t, ok, "expected true")
159+
})
134160
}

enterprise/wsproxy/mw.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package wsproxy
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net/http"
7+
8+
"github.com/coder/coder/coderd/httpapi"
9+
"github.com/coder/coder/coderd/httpmw"
10+
"github.com/coder/coder/codersdk"
11+
)
12+
13+
type userTokenKey struct{}
14+
15+
// UserSessionToken returns session token from ExtractSessionTokenMW
16+
func UserSessionToken(r *http.Request) string {
17+
key, ok := r.Context().Value(userTokenKey{}).(string)
18+
if !ok {
19+
panic("developer error: ExtractSessionTokenMW middleware not provided")
20+
}
21+
return key
22+
}
23+
24+
func ExtractSessionTokenMW() func(http.Handler) http.Handler {
25+
return func(next http.Handler) http.Handler {
26+
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
27+
token := httpmw.ApiTokenFromRequest(r)
28+
if token == "" {
29+
// TODO: If this is empty, we should attempt to smuggle their
30+
// token from the primary. If the user is not logged in there
31+
// they should be redirected to a login page.
32+
httpapi.Write(r.Context(), rw, http.StatusUnauthorized, codersdk.Response{
33+
Message: httpmw.SignedOutErrorMessage,
34+
Detail: fmt.Sprintf("Cookie %q or query parameter must be provided.", codersdk.SessionTokenCookie),
35+
})
36+
return
37+
}
38+
ctx := context.WithValue(r.Context(), userTokenKey{}, token)
39+
next.ServeHTTP(rw, r.WithContext(ctx))
40+
})
41+
}
42+
}

enterprise/wsproxy/proxy.go

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/google/uuid"
1212
"github.com/prometheus/client_golang/prometheus"
1313
"go.opentelemetry.io/otel/trace"
14+
"golang.org/x/xerrors"
1415

1516
"cdr.dev/slog"
1617
"github.com/coder/coder/buildinfo"
@@ -19,6 +20,7 @@ import (
1920
"github.com/coder/coder/coderd/workspaceapps"
2021
"github.com/coder/coder/coderd/wsconncache"
2122
"github.com/coder/coder/codersdk"
23+
"github.com/coder/coder/enterprise/wsproxy/wsproxysdk"
2224
)
2325

2426
type Options struct {
@@ -83,19 +85,22 @@ type Server struct {
8385
cancel context.CancelFunc
8486
}
8587

86-
func New(opts *Options) *Server {
88+
func New(opts *Options) (*Server, error) {
8789
if opts.PrometheusRegistry == nil {
8890
opts.PrometheusRegistry = prometheus.NewRegistry()
8991
}
9092

91-
client := codersdk.New(opts.PrimaryAccessURL)
93+
client := wsproxysdk.New(opts.PrimaryAccessURL)
9294
// TODO: @emyrk we need to implement some form of authentication for the
9395
// external proxy to the the primary. This allows us to make workspace
9496
// connections.
9597
// Ideally we reuse the same client as the cli, but this can be changed.
9698
// If the auth fails, we need some logic to retry and make sure this client
9799
// is always authenticated and usable.
98-
client.SetSessionToken("fake-token")
100+
err := client.SetSessionToken("fake-token")
101+
if err != nil {
102+
return nil, xerrors.Errorf("set client token: %w", err)
103+
}
99104

100105
r := chi.NewRouter()
101106
ctx, cancel := context.WithCancel(context.Background())
@@ -116,13 +121,19 @@ func New(opts *Options) *Server {
116121
AccessURL: opts.AccessURL,
117122
Hostname: opts.AppHostname,
118123
HostnameRegex: opts.AppHostnameRegex,
119-
// TODO: @emyrk We should reduce the options passed in here.
120-
DeploymentValues: nil,
121-
RealIPConfig: opts.RealIPConfig,
122-
// TODO: @emyrk we need to implement this for external token providers.
123-
SignedTokenProvider: nil,
124-
WorkspaceConnCache: wsconncache.New(s.DialWorkspaceAgent, 0),
125-
AppSecurityKey: opts.AppSecurityKey,
124+
RealIPConfig: opts.RealIPConfig,
125+
SignedTokenProvider: &ProxyTokenProvider{
126+
DashboardURL: opts.PrimaryAccessURL,
127+
Client: client,
128+
SecurityKey: s.Options.AppSecurityKey,
129+
Logger: s.Logger.Named("proxy_token_provider"),
130+
},
131+
WorkspaceConnCache: wsconncache.New(s.DialWorkspaceAgent, 0),
132+
AppSecurityKey: opts.AppSecurityKey,
133+
134+
// TODO: We need to pass some deployment values to here
135+
DisablePathApps: false,
136+
SecureAuthCookie: false,
126137
}
127138

128139
// Routes
@@ -137,6 +148,7 @@ func New(opts *Options) *Server {
137148
httpmw.ExtractRealIP(s.Options.RealIPConfig),
138149
httpmw.Logger(s.Logger),
139150
httpmw.Prometheus(s.PrometheusRegistry),
151+
ExtractSessionTokenMW(),
140152

141153
// SubdomainAppMW is a middleware that handles all requests to the
142154
// subdomain based workspace apps.
@@ -171,7 +183,7 @@ func New(opts *Options) *Server {
171183

172184
// TODO: @emyrk Buildinfo and healthz routes.
173185

174-
return s
186+
return s, nil
175187
}
176188

177189
func (s *Server) Close() error {

0 commit comments

Comments
 (0)