From 31b4a00a861c236ebf2229a42e2bdb6a3416fe7b Mon Sep 17 00:00:00 2001 From: Roman Tkachenko Date: Mon, 22 Aug 2022 13:43:50 -0700 Subject: [PATCH] (buddy) Pass JWT headers on websocket requests (#15667) * transport: Rewrite headers, including JWTs, for websockets. Applications can otherwise 401 on websocket requests, as they do not present any authentication headers. docs: Fix the reserved JWT header name. Signed-off-by: Roman Tkachenko * Add test for JWT header in websocket apps Signed-off-by: Roman Tkachenko Co-authored-by: Alex Vandiver --- .../guides/connecting-apps.mdx | 2 +- integration/app_integration_test.go | 42 ++++++++++++++++++- lib/srv/app/transport.go | 33 ++++++++------- 3 files changed, 61 insertions(+), 16 deletions(-) diff --git a/docs/pages/application-access/guides/connecting-apps.mdx b/docs/pages/application-access/guides/connecting-apps.mdx index 584ad1cb13f..1333d6f4c5a 100644 --- a/docs/pages/application-access/guides/connecting-apps.mdx +++ b/docs/pages/application-access/guides/connecting-apps.mdx @@ -283,7 +283,7 @@ requests forwarded to a web application. Headers injected this way override any headers with the same names that may be sent by an application. The following headers are reserved and can't be -rewritten: `X-Teleport-Jwt`, `Cf-Access-Token`, `X-Forwarded-*`. +rewritten: `Teleport-Jwt-Assertion`, `Cf-Access-Token`, and any `X-Forwarded-*`. Rewritten header values support the same templating variables as [role templates](../../access-controls/guides/role-templates.mdx). In the example above, `X-Internal-Trait` header will be populated with the value diff --git a/integration/app_integration_test.go b/integration/app_integration_test.go index 6e4fbab15be..c5d0b07aa8c 100644 --- a/integration/app_integration_test.go +++ b/integration/app_integration_test.go @@ -29,6 +29,7 @@ import ( "net" "net/http" "net/http/httptest" + "net/http/httputil" "net/url" "strings" "testing" @@ -365,6 +366,20 @@ func (p *pack) appAccessJWT(t *testing.T) { // Verify JWT token. verifyJWT(t, p, token, p.jwtAppURI) + + // Connect to websocket application that dumps the upgrade request. + wsCookie := p.createAppSession(t, p.wsHeaderAppPublicAddr, p.wsHeaderAppClusterName) + body, err := p.makeWebsocketRequest(wsCookie, "/") + require.NoError(t, err) + + // Parse the upgrade request the websocket application received. + req, err := http.ReadRequest(bufio.NewReader(strings.NewReader(body))) + require.NoError(t, err) + + // Extract JWT token from header and verify it. + wsToken := req.Header.Get(teleport.AppJWTHeader) + require.NotEmpty(t, wsToken, "websocket upgrade request doesn't contain JWT header") + verifyJWT(t, p, wsToken, p.wsHeaderAppURI) } func verifyJWT(t *testing.T, pack *pack, token, appURI string) { @@ -792,6 +807,11 @@ type pack struct { headerAppClusterName string headerAppURI string + wsHeaderAppName string + wsHeaderAppPublicAddr string + wsHeaderAppClusterName string + wsHeaderAppURI string + flushAppName string flushAppPublicAddr string flushAppClusterName string @@ -890,6 +910,10 @@ func setupWithOptions(t *testing.T, opts appTestOptions) *pack { headerAppPublicAddr: "app-04.example.com", headerAppClusterName: "example.com", + wsHeaderAppName: "ws-header", + wsHeaderAppPublicAddr: "ws-header.example.com", + wsHeaderAppClusterName: "example.com", + flushAppName: "app-05", flushAppPublicAddr: "app-05.example.com", flushAppClusterName: "example.com", @@ -901,7 +925,6 @@ func setupWithOptions(t *testing.T, opts appTestOptions) *pack { ReadBufferSize: 1024, WriteBufferSize: 1024, } - conn, err := upgrader.Upgrade(w, r, nil) require.NoError(t, err) handler(conn) @@ -954,10 +977,21 @@ func setupWithOptions(t *testing.T, opts appTestOptions) *pack { c.Close() }) t.Cleanup(func() { leafTCPServer.Close() }) + // JWT server writes generated JWT token in the response. jwtServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, r.Header.Get(teleport.AppJWTHeader)) })) t.Cleanup(jwtServer.Close) + // Websocket header server dumps initial HTTP upgrade request in the response. + wsHeaderServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + conn, err := (&websocket.Upgrader{}).Upgrade(w, r, nil) + require.NoError(t, err) + reqDump, err := httputil.DumpRequest(r, false) + require.NoError(t, err) + require.NoError(t, conn.WriteMessage(websocket.BinaryMessage, reqDump)) + require.NoError(t, conn.Close()) + })) + t.Cleanup(wsHeaderServer.Close) headerServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { for _, headerName := range forwardedHeaderNames { fmt.Fprintln(w, r.Header.Get(headerName)) @@ -999,6 +1033,7 @@ func setupWithOptions(t *testing.T, opts appTestOptions) *pack { p.leafTCPAppURI = fmt.Sprintf("tcp://%v", leafTCPServer.Addr().String()) p.jwtAppURI = jwtServer.URL p.headerAppURI = headerServer.URL + p.wsHeaderAppURI = wsHeaderServer.URL p.flushAppURI = flushServer.URL p.dumperAppURI = dumperServer.URL @@ -1544,6 +1579,11 @@ func (p *pack) startRootAppServers(t *testing.T, count int, extraApps []service. URI: p.headerAppURI, PublicAddr: p.headerAppPublicAddr, }, + { + Name: p.wsHeaderAppName, + URI: p.wsHeaderAppURI, + PublicAddr: p.wsHeaderAppPublicAddr, + }, { Name: p.flushAppName, URI: p.flushAppURI, diff --git a/lib/srv/app/transport.go b/lib/srv/app/transport.go index 48fad39748f..1bd09f084fa 100644 --- a/lib/srv/app/transport.go +++ b/lib/srv/app/transport.go @@ -115,7 +115,7 @@ func newTransport(ctx context.Context, c *transportConfig) (*transport, error) { c: c, uri: uri, tr: tr, - ws: newWebsocketTransport(uri, tr.TLSClientConfig), + ws: newWebsocketTransport(uri, tr.TLSClientConfig, c), }, nil } @@ -173,27 +173,28 @@ func (t *transport) rewriteRequest(r *http.Request) error { r.URL.Host = t.uri.Host // Add headers from rewrite configuration. - if t.c.app.GetRewrite() != nil && len(t.c.app.GetRewrite().Headers) > 0 { - t.rewriteHeaders(r) - } - - // Add in JWT headers. - r.Header.Set(teleport.AppJWTHeader, t.c.jwt) - r.Header.Set(teleport.AppCFHeader, t.c.jwt) + rewriteHeaders(r, t.c) return nil } // rewriteHeaders applies headers rewrites from the application configuration. -func (t *transport) rewriteHeaders(r *http.Request) { - for _, header := range t.c.app.GetRewrite().Headers { +func rewriteHeaders(r *http.Request, c *transportConfig) { + // Add in JWT headers. + r.Header.Set(teleport.AppJWTHeader, c.jwt) + r.Header.Set(teleport.AppCFHeader, c.jwt) + + if c.app.GetRewrite() == nil || len(c.app.GetRewrite().Headers) == 0 { + return + } + for _, header := range c.app.GetRewrite().Headers { if common.IsReservedHeader(header.Name) { - t.c.log.Debugf("Not rewriting Teleport header %q.", header.Name) + c.log.Debugf("Not rewriting Teleport header %q.", header.Name) continue } - values, err := services.ApplyValueTraits(header.Value, t.c.traits) + values, err := services.ApplyValueTraits(header.Value, c.traits) if err != nil { - t.c.log.Debugf("Failed to apply traits to %q: %v.", header.Value, err) + c.log.Debugf("Failed to apply traits to %q: %v.", header.Value, err) continue } r.Header.Del(header.Name) @@ -331,11 +332,12 @@ func isRedirect(code int) bool { type websocketTransport struct { uri *url.URL dialer forward.Dialer + c *transportConfig } // newWebsocketTransport returns transport that knows how to rewrite and // dial websocket requests. -func newWebsocketTransport(uri *url.URL, tlsConfig *tls.Config) *websocketTransport { +func newWebsocketTransport(uri *url.URL, tlsConfig *tls.Config, c *transportConfig) *websocketTransport { return &websocketTransport{ uri: uri, dialer: func(network, address string) (net.Conn, error) { @@ -346,6 +348,7 @@ func newWebsocketTransport(uri *url.URL, tlsConfig *tls.Config) *websocketTransp // Request is going to "ws://". return net.Dial(network, address) }, + c: c, } } @@ -359,4 +362,6 @@ func (r *websocketTransport) Rewrite(req *http.Request) { } req.URL.Host = r.uri.Host req.Host = r.uri.Host + + rewriteHeaders(req, r.c) }