From c1c0cbf5f85c81cf7e91f5623005c3c59c1a4e89 Mon Sep 17 00:00:00 2001 From: Ryan Clark Date: Thu, 12 Jan 2023 21:57:01 +0000 Subject: [PATCH 1/2] Change the application access authentication flow --- integration/helpers/cookies.go | 6 - lib/httplib/httpheaders.go | 33 +++++ lib/web/apiserver.go | 26 +++- lib/web/app/auth.go | 111 +++++++++++++++++ lib/web/app/fragment.go | 180 --------------------------- lib/web/app/handler.go | 60 ++++++++- lib/web/app/handler_test.go | 218 ++++++++++++++++++++++++--------- lib/web/app/redirect.go | 70 ----------- lib/web/app/transport.go | 2 +- lib/web/apps.go | 4 +- 10 files changed, 380 insertions(+), 330 deletions(-) create mode 100644 lib/web/app/auth.go delete mode 100644 lib/web/app/fragment.go diff --git a/integration/helpers/cookies.go b/integration/helpers/cookies.go index 51f4a128b7cb4..803f5ee93c411 100644 --- a/integration/helpers/cookies.go +++ b/integration/helpers/cookies.go @@ -25,7 +25,6 @@ import ( type AppCookies struct { SessionCookie *http.Cookie SubjectSessionCookie *http.Cookie - AuthStateCookie *http.Cookie } // WithSubjectCookie returns a copy of AppCookies with the specified subject session cookie. @@ -44,9 +43,6 @@ func (ac *AppCookies) ToSlice() []*http.Cookie { if ac.SubjectSessionCookie != nil { out = append(out, ac.SubjectSessionCookie) } - if ac.AuthStateCookie != nil { - out = append(out, ac.AuthStateCookie) - } return out } @@ -60,8 +56,6 @@ func ParseCookies(t *testing.T, cookies []*http.Cookie) *AppCookies { out.SessionCookie = c case app.SubjectCookieName: out.SubjectSessionCookie = c - case app.AuthStateCookieName: - out.AuthStateCookie = c default: t.Fatalf("unrecognized cookie name: %q", c.Name) } diff --git a/lib/httplib/httpheaders.go b/lib/httplib/httpheaders.go index 21e57c06d8dfc..63f0319fbc393 100644 --- a/lib/httplib/httpheaders.go +++ b/lib/httplib/httpheaders.go @@ -19,6 +19,7 @@ limitations under the License. package httplib import ( + "fmt" "net/http" "strings" ) @@ -90,6 +91,38 @@ func SetNoSniff(h http.Header) { h.Set("X-Content-Type-Options", "nosniff") } +// SetAppLaunchContentSecurityPolicy sets the Content-Security-Policy header for /web/launch +func SetAppLaunchContentSecurityPolicy(h http.Header, applicationURL string) { + var cspValue = strings.Join([]string{ + GetDefaultContentSecurityPolicy(), + // 'unsafe-inline' is required by CSS-in-JS to work + "style-src 'self' 'unsafe-inline'", + "img-src 'self' data: blob:", + "font-src 'self' data:", + fmt.Sprintf("connect-src 'self' %s", applicationURL), + }, ";") + + h.Set("Content-Security-Policy", cspValue) +} + +// GetDefaultContentSecurityPolicy provides a starting Content Security Policy with safe defaults. +func GetDefaultContentSecurityPolicy() string { + return strings.Join([]string{ + "default-src 'self'", + // specify CSP directives not covered by `default-src` + "base-uri 'self'", + "form-action 'self'", + "frame-ancestors 'none'", + // additional default restrictions + "object-src 'none'", + }, ";") +} + +// SetDefaultContentSecurityPolicy provides a starting Content Security Policy with safe defaults. +func SetDefaultContentSecurityPolicy(h http.Header) { + h.Set("Content-Security-Policy", GetDefaultContentSecurityPolicy()) +} + // SetWebConfigHeaders sets headers for webConfig.js func SetWebConfigHeaders(h http.Header) { SetStaticFileHeaders(h) diff --git a/lib/web/apiserver.go b/lib/web/apiserver.go index 084c1dbc8d57b..d495ce1af3191 100644 --- a/lib/web/apiserver.go +++ b/lib/web/apiserver.go @@ -247,14 +247,17 @@ func (h *APIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // request has a session cookie or a client cert, forward to // application handlers. If the request is requesting a // FQDN that is not of the proxy, redirect to application launcher. + if h.appHandler != nil && (app.HasFragment(r) || app.HasSession(r) || app.HasClientCert(r)) { h.appHandler.ServeHTTP(w, r) return } + if redir, ok := app.HasName(r, h.handler.cfg.ProxyPublicAddrs); ok { http.Redirect(w, r, redir, http.StatusFound) return } + // Serve the Web UI. h.handler.ServeHTTP(w, r) } @@ -394,6 +397,16 @@ func NewHandler(cfg Config, opts ...HandlerOption) (*APIHandler, error) { session.XCSRF = csrfToken httplib.SetIndexHTMLHeaders(w.Header()) + + // app access needs to make a CORS fetch request, so we only set the default CSP on that page + if strings.HasPrefix(r.URL.Path, "/web/launch") { + parts := strings.Split(r.URL.Path, "/") + // grab the FQDN from the URL to allow in the connect-src CSP + applicationURL := "https://" + parts[3] + ":*" + + httplib.SetAppLaunchContentSecurityPolicy(w.Header(), applicationURL) + } + if err := indexPage.Execute(w, session); err != nil { h.log.WithError(err).Error("Failed to execute index page template.") } @@ -420,12 +433,13 @@ func NewHandler(cfg Config, opts ...HandlerOption) (*APIHandler, error) { var appHandler *app.Handler if !cfg.MinimalReverseTunnelRoutesOnly { appHandler, err = app.NewHandler(cfg.Context, &app.HandlerConfig{ - Clock: h.clock, - AuthClient: cfg.ProxyClient, - AccessPoint: cfg.AccessPoint, - ProxyClient: cfg.Proxy, - CipherSuites: cfg.CipherSuites, - WebPublicAddr: resp.SSH.PublicAddr, + Clock: h.clock, + AuthClient: cfg.ProxyClient, + AccessPoint: cfg.AccessPoint, + ProxyClient: cfg.Proxy, + CipherSuites: cfg.CipherSuites, + ProxyPublicAddrs: cfg.ProxyPublicAddrs, + WebPublicAddr: resp.SSH.PublicAddr, }) if err != nil { return nil, trace.Wrap(err) diff --git a/lib/web/app/auth.go b/lib/web/app/auth.go new file mode 100644 index 0000000000000..10a61d167f1a0 --- /dev/null +++ b/lib/web/app/auth.go @@ -0,0 +1,111 @@ +/* +Copyright 2022 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package app + +import ( + "crypto/subtle" + "net/http" + + "github.com/gravitational/trace" + "github.com/julienschmidt/httprouter" + + "github.com/gravitational/teleport/api/types" + apievents "github.com/gravitational/teleport/api/types/events" + "github.com/gravitational/teleport/lib/events" + "github.com/gravitational/teleport/lib/httplib" +) + +// handleAuth handles authentication for an app +// When a `POST` request comes in from a trusted proxy address, it'll set the value from the +// `X-Cookie-Value` header to the `__Host-grv_app_session` cookie. +func (h *Handler) handleAuth(w http.ResponseWriter, r *http.Request, p httprouter.Params) error { + httplib.SetNoCacheHeaders(w.Header()) + + cookieValue := r.Header.Get("X-Cookie-Value") + if cookieValue == "" { + return trace.AccessDenied("access denied") + } + + subjectCookieValue := r.Header.Get("X-Subject-Cookie-Value") + if cookieValue == "" { + return trace.BadParameter("X-Subject-Cookie-Value header missing") + } + + // Validate that the caller is asking for a session that exists. + ws, err := h.c.AccessPoint.GetAppSession(r.Context(), types.GetAppSessionRequest{ + SessionID: cookieValue, + }) + if err != nil { + h.log.WithError(err).Warn("Request failed: unable to get app session") + return trace.AccessDenied("access denied") + } + + if err := checkSubjectToken(subjectCookieValue, ws); err != nil { + h.log.Warnf("Request failed: %v.", err) + + h.c.AuthClient.EmitAuditEvent(h.closeContext, &apievents.AuthAttempt{ + Metadata: apievents.Metadata{ + Type: events.AuthAttemptEvent, + Code: events.AuthAttemptFailureCode, + }, + UserMetadata: apievents.UserMetadata{ + Login: ws.GetUser(), + User: "unknown", + }, + ConnectionMetadata: apievents.ConnectionMetadata{ + LocalAddr: r.Host, + RemoteAddr: r.RemoteAddr, + }, + Status: apievents.Status{ + Success: false, + Error: err.Error(), + }, + }) + + return trace.AccessDenied("access denied") + } + + http.SetCookie(w, &http.Cookie{ + Name: CookieName, + Value: cookieValue, + Path: "/", + HttpOnly: true, + Secure: true, + SameSite: http.SameSiteLaxMode, + }) + + http.SetCookie(w, &http.Cookie{ + Name: SubjectCookieName, + Value: subjectCookieValue, + Path: "/", + HttpOnly: true, + Secure: true, + SameSite: http.SameSiteLaxMode, + }) + + return nil +} + +func checkSubjectToken(subjectCookieValue string, ws types.WebSession) error { + if subjectCookieValue == "" { + return trace.AccessDenied("subject session token is not set") + } + if subtle.ConstantTimeCompare([]byte(subjectCookieValue), []byte(ws.GetBearerToken())) != 1 { + return trace.AccessDenied("subject session token does not match") + } + return nil +} diff --git a/lib/web/app/fragment.go b/lib/web/app/fragment.go deleted file mode 100644 index 6063c15d22ad8..0000000000000 --- a/lib/web/app/fragment.go +++ /dev/null @@ -1,180 +0,0 @@ -/* -Copyright 2020 Gravitational, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package app - -import ( - "crypto/subtle" - "encoding/json" - "fmt" - "net/http" - "time" - - "github.com/gravitational/trace" - "github.com/julienschmidt/httprouter" - - "github.com/gravitational/teleport/api/types" - apievents "github.com/gravitational/teleport/api/types/events" - "github.com/gravitational/teleport/lib/auth" - "github.com/gravitational/teleport/lib/events" - "github.com/gravitational/teleport/lib/httplib" - "github.com/gravitational/teleport/lib/utils" -) - -type fragmentRequest struct { - StateValue string `json:"state_value"` - CookieValue string `json:"cookie_value"` - SubjectCookieValue string `json:"subject_cookie_value"` -} - -// handleFragment handles fragment authentication. Returns a Javascript -// application that reads in the fragment which submits an POST request to -// the same handler which can validate and set the cookie. -func (h *Handler) handleFragment(w http.ResponseWriter, r *http.Request, p httprouter.Params) error { - switch r.Method { - case http.MethodGet: - q := r.URL.Query() - // If the state query parameter is not set, generate a new state token, - // store it in a cookie and redirect back to the app launcher. - if q.Get("state") == "" { - stateToken, err := utils.CryptoRandomHex(auth.TokenLenBytes) - if err != nil { - h.log.WithError(err).Debugf("Failed to generate and encode random numbers.") - return trace.AccessDenied("access denied") - } - h.setAuthStateCookie(w, stateToken) - urlParams := launcherURLParams{ - clusterName: q.Get("cluster"), - publicAddr: q.Get("addr"), - awsRole: q.Get("awsrole"), - path: q.Get("path"), - stateToken: stateToken, - } - return h.redirectToLauncher(w, r, urlParams) - } - - nonce, err := utils.CryptoRandomHex(auth.TokenLenBytes) - if err != nil { - h.log.WithError(err).Debugf("Failed to generate and encode random numbers.") - return trace.AccessDenied("access denied") - } - SetRedirectPageHeaders(w.Header(), nonce) - fmt.Fprintf(w, js, nonce) - return nil - - case http.MethodPost: - httplib.SetNoCacheHeaders(w.Header()) - var req fragmentRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - return trace.Wrap(err) - } - - // Validate that the caller-provided state token matches the stored state token. - stateCookie, err := r.Cookie(AuthStateCookieName) - if err != nil || stateCookie.Value == "" { - h.log.Warn("Request failed: state cookie is not set.") - return trace.AccessDenied("access denied") - } - if subtle.ConstantTimeCompare([]byte(req.StateValue), []byte(stateCookie.Value)) != 1 { - h.log.Warn("Request failed: state token does not match.") - return trace.AccessDenied("access denied") - } - - // Prevent reuse of the same state token. - h.setAuthStateCookie(w, "") - - // Validate that the caller is asking for a session that exists and that they have the secret - // session token for. - ws, err := h.c.AccessPoint.GetAppSession(r.Context(), types.GetAppSessionRequest{ - SessionID: req.CookieValue, - }) - if err != nil { - h.log.Warn("Request failed: session does not exist.") - return trace.AccessDenied("access denied") - } - if err := checkSubjectToken(&req, ws); err != nil { - h.log.Warnf("Request failed: %v.", err) - h.c.AuthClient.EmitAuditEvent(h.closeContext, &apievents.AuthAttempt{ - Metadata: apievents.Metadata{ - Type: events.AuthAttemptEvent, - Code: events.AuthAttemptFailureCode, - }, - UserMetadata: apievents.UserMetadata{ - Login: ws.GetUser(), - User: "unknown", - }, - ConnectionMetadata: apievents.ConnectionMetadata{ - LocalAddr: r.Host, - RemoteAddr: r.RemoteAddr, - }, - Status: apievents.Status{ - Success: false, - Error: err.Error(), - }, - }) - return trace.AccessDenied("access denied") - } - - // Set the "Set-Cookie" header on the response. - // Set Same-Site policy for the session cookies to None in order to - // support redirects that identity providers do during SSO auth. - // Otherwise the session cookie won't be sent and the user will - // get redirected to the application launcher. - // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite - http.SetCookie(w, &http.Cookie{ - Name: CookieName, - Value: req.CookieValue, - Path: "/", - HttpOnly: true, - Secure: true, - SameSite: http.SameSiteNoneMode, - }) - http.SetCookie(w, &http.Cookie{ - Name: SubjectCookieName, - Value: ws.GetBearerToken(), - Path: "/", - HttpOnly: true, - Secure: true, - SameSite: http.SameSiteNoneMode, - }) - return nil - default: - return trace.BadParameter("unsupported method %q", r.Method) - } -} - -// checkSubjectToken checks that the subject cookie value in the fragment request is not empty and matches the session bearer token. -func checkSubjectToken(req *fragmentRequest, ws types.WebSession) error { - if req.SubjectCookieValue == "" { - return trace.AccessDenied("subject session token is not set") - } - if subtle.ConstantTimeCompare([]byte(req.SubjectCookieValue), []byte(ws.GetBearerToken())) != 1 { - return trace.AccessDenied("subject session token does not match") - } - return nil -} - -func (h *Handler) setAuthStateCookie(w http.ResponseWriter, value string) { - http.SetCookie(w, &http.Cookie{ - Name: AuthStateCookieName, - Value: value, - Path: "/", - HttpOnly: true, - Secure: true, - SameSite: http.SameSiteNoneMode, - Expires: h.c.Clock.Now().UTC().Add(1 * time.Minute), - }) -} diff --git a/lib/web/app/handler.go b/lib/web/app/handler.go index 1c4ba27b2acd6..af97772050df8 100644 --- a/lib/web/app/handler.go +++ b/lib/web/app/handler.go @@ -25,6 +25,7 @@ import ( "net" "net/http" "net/url" + "strconv" oxyutils "github.com/gravitational/oxy/utils" "github.com/gravitational/trace" @@ -52,6 +53,8 @@ type HandlerConfig struct { AccessPoint auth.ProxyAccessPoint // ProxyClient holds connections to leaf clusters. ProxyClient reversetunnel.Tunnel + // ProxyPublicAddrs contains web proxy public addresses. + ProxyPublicAddrs []utils.NetAddr // CipherSuites is the list of TLS cipher suites that have been configured // for this process. CipherSuites []uint16 @@ -125,8 +128,7 @@ func NewHandler(ctx context.Context, c *HandlerConfig) (*Handler, error) { // Create the application routes. h.router = httprouter.New() h.router.UseRawPath = true - h.router.GET("/x-teleport-auth", makeRouterHandler(h.handleFragment)) - h.router.POST("/x-teleport-auth", makeRouterHandler(h.handleFragment)) + h.router.POST("/x-teleport-auth", makeRouterHandler(h.handleAuth)) h.router.GET("/teleport-logout", h.withRouterAuth(h.handleLogout)) h.router.NotFound = h.withAuth(h.handleForward) @@ -135,6 +137,56 @@ func NewHandler(ctx context.Context, c *HandlerConfig) (*Handler, error) { // ServeHTTP hands the request to the request router. func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/x-teleport-auth" { + // Allow minimal CORS from only the proxy origin + // This allows for requests from the proxy to `POST` to `/x-teleport-auth` and only + // permits the headers `X-Cookie-Value` and `X-Subject-Cookie-Value`. + // This is for the web UI to post a request to the application to get the proper app session + // cookie set on the right application subdomain. + w.Header().Set("Access-Control-Allow-Methods", "POST") + w.Header().Set("Access-Control-Allow-Credentials", "true") + w.Header().Set("Access-Control-Allow-Headers", "X-Cookie-Value, X-Subject-Cookie-Value") + + // Validate that the origin for the request matches any of the public proxy addresses. + // This is instead of protecting via CORS headers, as that only supports a single domain. + originValue := r.Header.Get("Origin") + origin, err := url.Parse(originValue) + if err != nil { + h.log.Errorf("malformed Origin header: %v", err) + + w.WriteHeader(http.StatusBadRequest) + + return + } + + var match bool + originPort := origin.Port() + if originPort == "" { + originPort = "443" + } + + for _, addr := range h.c.ProxyPublicAddrs { + if strconv.Itoa(addr.Port(0)) == originPort && addr.Host() == origin.Hostname() { + match = true + break + } + } + + if !match { + w.WriteHeader(http.StatusForbidden) + + return + } + + // As we've already checked the origin matches a public proxy address, we can allow requests from that origin + // We do this dynamically as this header can only contain one value + w.Header().Set("Access-Control-Allow-Origin", originValue) + + if r.Method == http.MethodOptions { + return + } + } + h.router.ServeHTTP(w, r) } @@ -481,8 +533,4 @@ const ( // SubjectCookieName is the name of the application session subject cookie. SubjectCookieName = "__Host-grv_app_session_subject" - - // AuthStateCookieName is the name of the state cookie used during the - // initial authentication flow. - AuthStateCookieName = "__Host-grv_app_auth_state" ) diff --git a/lib/web/app/handler_test.go b/lib/web/app/handler_test.go index 723a6ad455ab2..da21aef87abef 100644 --- a/lib/web/app/handler_test.go +++ b/lib/web/app/handler_test.go @@ -21,7 +21,6 @@ import ( "context" "crypto/tls" "crypto/x509/pkix" - "encoding/json" "fmt" "io" "net" @@ -74,13 +73,13 @@ func hasAuditEventCount(want int) eventCheckFn { // TestAuthPOST tests the handler of POST /x-teleport-auth. func TestAuthPOST(t *testing.T) { const ( - stateValue = "012ac605867e5a7d693cd6f49c7ff0fb" - cookieValue = "5588e2be54a2834b4f152c56bafcd789f53b15477129d2ab4044e9a3c1bf0f3b" + cookieValue = "5588e2be54a2834b4f152c56bafcd789f53b15477129d2ab4044e9a3c1bf0f3b" // random value we set in the header and expect to get back as a cookie ) fakeClock := clockwork.NewFakeClockAt(time.Date(2017, 05, 10, 18, 53, 0, 0, time.UTC)) clusterName := "test-cluster" - publicAddr := "app.example.com" + publicAddr := "proxy.goteleport.com:443" + // Generate CA TLS key and cert with the cluster and application DNS. key, cert, err := tlsca.GenerateSelfSignedCA( pkix.Name{CommonName: clusterName}, @@ -88,39 +87,56 @@ func TestAuthPOST(t *testing.T) { defaults.CATTL, ) require.NoError(t, err) + appSession := createAppSession(t, fakeClock, key, cert, clusterName, publicAddr) tests := []struct { - desc string - stateInRequest string - stateInCookie string - subjectInRequest string - sessionError error - outStatusCode int - eventChecks []eventCheckFn + desc string + headers map[string]string + sessionError error + outStatusCode int + eventChecks []eventCheckFn + proxyAddrs []utils.NetAddr + cookieValue string + subjectCookieValue string }{ { - desc: "success", - stateInRequest: stateValue, - stateInCookie: stateValue, - subjectInRequest: appSession.GetBearerToken(), - outStatusCode: http.StatusOK, - eventChecks: []eventCheckFn{hasAuditEventCount(0)}, + desc: "success", + headers: map[string]string{ + "Origin": "https://proxy.goteleport.com", + "X-Cookie-Value": cookieValue, + "X-Subject-Cookie-Value": appSession.GetBearerToken(), + }, + outStatusCode: http.StatusOK, + eventChecks: []eventCheckFn{hasAuditEventCount(0)}, + proxyAddrs: []utils.NetAddr{ + *utils.MustParseAddr(publicAddr), + }, + cookieValue: cookieValue, + subjectCookieValue: appSession.GetBearerToken(), }, { - desc: "missing state token in request", - stateInRequest: "", - stateInCookie: stateValue, - subjectInRequest: appSession.GetBearerToken(), - outStatusCode: http.StatusForbidden, - eventChecks: []eventCheckFn{hasAuditEventCount(0)}, + desc: "success - proxy addr with custom port", + headers: map[string]string{ + "Origin": "https://proxy.goteleport.com:3080", + "X-Cookie-Value": cookieValue, + "X-Subject-Cookie-Value": appSession.GetBearerToken(), + }, + outStatusCode: http.StatusOK, + eventChecks: []eventCheckFn{hasAuditEventCount(0)}, + proxyAddrs: []utils.NetAddr{ + *utils.MustParseAddr("proxy.goteleport.com:3080"), + }, + cookieValue: cookieValue, + subjectCookieValue: appSession.GetBearerToken(), }, { - desc: "missing subject session token in request", - stateInRequest: stateValue, - stateInCookie: stateValue, - subjectInRequest: "", - outStatusCode: http.StatusForbidden, + desc: "missing subject session token in request", + headers: map[string]string{ + "Origin": "https://proxy.goteleport.com", + "X-Cookie-Value": cookieValue, + }, + outStatusCode: http.StatusForbidden, eventChecks: []eventCheckFn{ hasAuditEventCount(1), hasAuditEvent(0, &apievents.AuthAttempt{ @@ -138,13 +154,18 @@ func TestAuthPOST(t *testing.T) { }, }), }, + proxyAddrs: []utils.NetAddr{ + *utils.MustParseAddr(publicAddr), + }, }, { - desc: "subject session token in request does not match", - stateInRequest: stateValue, - stateInCookie: stateValue, - subjectInRequest: "foobar", - outStatusCode: http.StatusForbidden, + desc: "subject session token in request does not match", + headers: map[string]string{ + "Origin": "https://proxy.goteleport.com", + "X-Cookie-Value": cookieValue, + "X-Subject-Cookie-Value": "foobar", + }, + outStatusCode: http.StatusForbidden, eventChecks: []eventCheckFn{ hasAuditEventCount(1), hasAuditEvent(0, &apievents.AuthAttempt{ @@ -162,39 +183,84 @@ func TestAuthPOST(t *testing.T) { }, }), }, + proxyAddrs: []utils.NetAddr{ + *utils.MustParseAddr(publicAddr), + }, }, { - desc: "invalid session", - stateInRequest: stateValue, - stateInCookie: stateValue, - subjectInRequest: appSession.GetBearerToken(), - sessionError: trace.NotFound("invalid session"), - outStatusCode: http.StatusForbidden, - eventChecks: []eventCheckFn{hasAuditEventCount(0)}, + desc: "invalid session", + headers: map[string]string{ + "Origin": "https://proxy.goteleport.com", + "X-Cookie-Value": "foobar", + "X-Subject-Cookie-Value": appSession.GetBearerToken(), + }, + sessionError: trace.NotFound("invalid session"), + outStatusCode: http.StatusForbidden, + eventChecks: []eventCheckFn{hasAuditEventCount(0)}, + proxyAddrs: []utils.NetAddr{ + *utils.MustParseAddr(publicAddr), + }, + }, + { + desc: "incorrect origin", + headers: map[string]string{ + "Origin": "https://incorrect.origin.com", + "X-Cookie-Value": "foobar", + "X-Subject-Cookie-Value": appSession.GetBearerToken(), + }, + outStatusCode: http.StatusForbidden, + eventChecks: []eventCheckFn{hasAuditEventCount(0)}, + proxyAddrs: []utils.NetAddr{ + *utils.MustParseAddr(publicAddr), + }, + }, + { + desc: "incorrect origin port", + headers: map[string]string{ + "Origin": "https://proxy.goteleport.com:3080", + "X-Cookie-Value": "foobar", + "X-Subject-Cookie-Value": appSession.GetBearerToken(), + }, + outStatusCode: http.StatusForbidden, + eventChecks: []eventCheckFn{hasAuditEventCount(0)}, + proxyAddrs: []utils.NetAddr{ + *utils.MustParseAddr(publicAddr), + }, }, } + for _, test := range tests { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() + authClient := &mockAuthClient{ sessionError: test.sessionError, appSession: appSession, } - p := setup(t, fakeClock, authClient, nil) - req, err := json.Marshal(fragmentRequest{ - StateValue: test.stateInRequest, - CookieValue: cookieValue, - SubjectCookieValue: test.subjectInRequest, - }) - require.NoError(t, err) + p := setup(t, fakeClock, authClient, nil, test.proxyAddrs) + + res := p.makeRequestWithHeaders(t, "/x-teleport-auth", test.headers) + + require.NoError(t, res.Body.Close()) + require.Equal(t, test.outStatusCode, res.StatusCode) + + var cookieValue string + var subjectCookieValue string + for _, cookie := range res.Cookies() { + if cookie.Name == CookieName { + cookieValue = cookie.Value + } + + if cookie.Name == SubjectCookieName { + subjectCookieValue = cookie.Value + } + } + + require.Equal(t, subjectCookieValue, test.subjectCookieValue) + require.Equal(t, cookieValue, test.cookieValue) - status, _ := p.makeRequest(t, "POST", "/x-teleport-auth", req, []http.Cookie{{ - Name: AuthStateCookieName, - Value: test.stateInCookie, - }}) - require.Equal(t, test.outStatusCode, status) for _, check := range test.eventChecks { check(t, authClient.emittedEvents) } @@ -312,7 +378,7 @@ func TestMatchApplicationServers(t *testing.T) { server.Close() }) - p := setup(t, fakeClock, authClient, tunnel) + p := setup(t, fakeClock, authClient, tunnel, nil) status, content := p.makeRequest(t, "GET", "/", []byte{}, []http.Cookie{ { Name: CookieName, @@ -323,6 +389,7 @@ func TestMatchApplicationServers(t *testing.T) { Value: authClient.appSession.GetBearerToken(), }, }) + require.Equal(t, http.StatusOK, status) // Remote site should receive only 4 connection requests: 3 from the // MatchHealthy and 1 from the transport. @@ -335,13 +402,14 @@ type testServer struct { serverURL *url.URL } -func setup(t *testing.T, clock clockwork.FakeClock, authClient auth.ClientI, proxyClient reversetunnel.Tunnel) *testServer { +func setup(t *testing.T, clock clockwork.FakeClock, authClient auth.ClientI, proxyClient reversetunnel.Tunnel, proxyPublicAddrs []utils.NetAddr) *testServer { appHandler, err := NewHandler(context.Background(), &HandlerConfig{ - Clock: clock, - AuthClient: authClient, - AccessPoint: authClient, - ProxyClient: proxyClient, - CipherSuites: utils.DefaultCipherSuites(), + Clock: clock, + AuthClient: authClient, + AccessPoint: authClient, + ProxyClient: proxyClient, + CipherSuites: utils.DefaultCipherSuites(), + ProxyPublicAddrs: proxyPublicAddrs, }) require.NoError(t, err) @@ -392,6 +460,38 @@ func (p *testServer) makeRequest(t *testing.T, method, endpoint string, reqBody return resp.StatusCode, string(content) } +func (p *testServer) makeRequestWithHeaders(t *testing.T, endpoint string, headers map[string]string) *http.Response { + u := url.URL{ + Scheme: p.serverURL.Scheme, + Host: p.serverURL.Host, + Path: endpoint, + } + req, err := http.NewRequest(http.MethodPost, u.String(), nil) + require.NoError(t, err) + + for key, value := range headers { + req.Header.Add(key, value) + } + + // Issue request. + client := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + }, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + } + + resp, err := client.Do(req) + require.NoError(t, err) + require.NoError(t, resp.Body.Close()) + + return resp +} + type mockAuthClient struct { auth.ClientI clusterName string diff --git a/lib/web/app/redirect.go b/lib/web/app/redirect.go index 2a93d8cdab469..f18c1599550d6 100644 --- a/lib/web/app/redirect.go +++ b/lib/web/app/redirect.go @@ -43,73 +43,3 @@ func SetRedirectPageHeaders(h http.Header, nonce string) { h.Set("Referrer-Policy", "no-referrer") h.Set("Content-Security-Policy", csp) } - -const js = ` - - - - Teleport Redirection Service - - - - -` diff --git a/lib/web/app/transport.go b/lib/web/app/transport.go index 6e26ec653c420..689dfa455be80 100644 --- a/lib/web/app/transport.go +++ b/lib/web/app/transport.go @@ -158,7 +158,7 @@ func (t *transport) rewriteRequest(r *http.Request) error { r.Header.Del("Cookie") for _, cookie := range cookies { switch cookie.Name { - case CookieName, SubjectCookieName, AuthStateCookieName: + case CookieName, SubjectCookieName: continue default: r.AddCookie(cookie) diff --git a/lib/web/apps.go b/lib/web/apps.go index 0467a84055d8e..381ead85de8a4 100644 --- a/lib/web/apps.go +++ b/lib/web/apps.go @@ -89,9 +89,9 @@ type CreateAppSessionRequest resolveAppParams type CreateAppSessionResponse struct { // CookieValue is the application session cookie value. - CookieValue string `json:"value"` + CookieValue string `json:"cookie_value"` // SubjectCookieValue is the application session subject cookie token. - SubjectCookieValue string `json:"subject"` + SubjectCookieValue string `json:"subject_cookie_value"` // FQDN is application FQDN. FQDN string `json:"fqdn"` } From 4a50fb22585ed2f1ace0bc0eb751daf3dd8f1d54 Mon Sep 17 00:00:00 2001 From: Ryan Clark Date: Thu, 12 Jan 2023 22:59:13 +0000 Subject: [PATCH 2/2] Update webassets ref --- webassets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webassets b/webassets index 099ee2dafb37a..b9ffa02cefae7 160000 --- a/webassets +++ b/webassets @@ -1 +1 @@ -Subproject commit 099ee2dafb37a2ea624f3de22daa76a158c0e1c1 +Subproject commit b9ffa02cefae734b6b0ff0403ef961f4fa454c92