mirror of
https://github.com/gravitational/teleport.git
synced 2026-11-05 03:33:24 +08:00
(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 <roman@goteleport.com> * Add test for JWT header in websocket apps Signed-off-by: Roman Tkachenko <roman@goteleport.com> Co-authored-by: Alex Vandiver <alex@chmrr.net>
This commit is contained in:
co-authored by
Alex Vandiver
parent
91c89b6126
commit
31b4a00a86
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
+19
-14
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user