Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions integration/helpers/cookies.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
}

Expand All @@ -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)
}
Expand Down
33 changes: 33 additions & 0 deletions lib/httplib/httpheaders.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ limitations under the License.
package httplib

import (
"fmt"
"net/http"
"strings"
)
Expand Down Expand Up @@ -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)
Expand Down
26 changes: 20 additions & 6 deletions lib/web/apiserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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.")
}
Expand All @@ -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)
Expand Down
111 changes: 111 additions & 0 deletions lib/web/app/auth.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading