Skip to content

Commit

Permalink
[v16] Automatically authenticate required applications during web app…
Browse files Browse the repository at this point in the history
… auth (#46366)
  • Loading branch information
avatus authored Sep 18, 2024
1 parent ca3b39a commit db6523e
Show file tree
Hide file tree
Showing 22 changed files with 1,754 additions and 1,427 deletions.
3 changes: 3 additions & 0 deletions api/proto/teleport/legacy/types/types.proto
Original file line number Diff line number Diff line change
Expand Up @@ -969,6 +969,9 @@ message AppSpecV3 {
// Only applicable to AWS App Access.
// If present, the Application must use the Integration's credentials instead of ambient credentials to access Cloud APIs.
string Integration = 9 [(gogoproto.jsontag) = "integration,omitempty"];
// RequiredAppNames is a list of app names that are required for this app to function. Any app listed here will
// be part of the authentication redirect flow and authenticate along side this app.
repeated string RequiredAppNames = 10 [(gogoproto.jsontag) = "required_app_names,omitempty"];
}

// AppServerOrSAMLIdPServiceProviderV1 holds either an AppServerV3 or a SAMLIdPServiceProviderV1 resource (never both).
Expand Down
6 changes: 6 additions & 0 deletions api/types/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ type Application interface {
// GetIntegration will return the Integration.
// If present, the Application must use the Integration's credentials instead of ambient credentials to access Cloud APIs.
GetIntegration() string
// GetRequiredAppNames will return a list of required apps names that should be authenticated during this apps authentication process.
GetRequiredAppNames() []string
}

// NewAppV3 creates a new app resource.
Expand Down Expand Up @@ -319,6 +321,10 @@ func (a *AppV3) Copy() *AppV3 {
return utils.CloneProtoMsg(a)
}

func (a *AppV3) GetRequiredAppNames() []string {
return a.Spec.RequiredAppNames
}

// MatchSearch goes through select field values and tries to
// match against the list of search values.
func (a *AppV3) MatchSearch(values []string) bool {
Expand Down
3 changes: 2 additions & 1 deletion api/types/derived.gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2,830 changes: 1,441 additions & 1,389 deletions api/types/types.pb.go

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ Optional:
- `insecure_skip_verify` (Boolean) InsecureSkipVerify disables app's TLS certificate verification.
- `integration` (String) Integration is the integration name that must be used to access this Application. Only applicable to AWS App Access. If present, the Application must use the Integration's credentials instead of ambient credentials to access Cloud APIs.
- `public_addr` (String) PublicAddr is the public address the application is accessible at.
- `required_app_names` (List of String) RequiredAppNames is a list of app names that are required for this app to function. Any app listed here will be part of the authentication redirect flow and authenticate along side this app.
- `rewrite` (Attributes) Rewrite is a list of rewriting rules to apply to requests and responses. (see [below for nested schema](#nested-schema-for-specrewrite))
- `uri` (String) URI is the web app endpoint.
- `user_groups` (List of String) UserGroups are a list of user group IDs that this app is associated with.
Expand Down
1 change: 1 addition & 0 deletions docs/pages/reference/terraform-provider/resources/app.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ Optional:
- `insecure_skip_verify` (Boolean) InsecureSkipVerify disables app's TLS certificate verification.
- `integration` (String) Integration is the integration name that must be used to access this Application. Only applicable to AWS App Access. If present, the Application must use the Integration's credentials instead of ambient credentials to access Cloud APIs.
- `public_addr` (String) PublicAddr is the public address the application is accessible at.
- `required_app_names` (List of String) RequiredAppNames is a list of app names that are required for this app to function. Any app listed here will be part of the authentication redirect flow and authenticate along side this app.
- `rewrite` (Attributes) Rewrite is a list of rewriting rules to apply to requests and responses. (see [below for nested schema](#nested-schema-for-specrewrite))
- `uri` (String) URI is the web app endpoint.
- `user_groups` (List of String) UserGroups are a list of user group IDs that this app is associated with.
Expand Down
85 changes: 85 additions & 0 deletions integrations/terraform/tfschema/types_terraform.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions lib/config/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -2004,6 +2004,7 @@ func applyAppsConfig(fc *FileConfig, cfg *servicecfg.Config) error {
DynamicLabels: dynamicLabels,
InsecureSkipVerify: application.InsecureSkipVerify,
Cloud: application.Cloud,
RequiredAppNames: application.RequiredApps,
}
if application.Rewrite != nil {
// Parse http rewrite headers if there are any.
Expand Down
4 changes: 4 additions & 0 deletions lib/config/fileconf.go
Original file line number Diff line number Diff line change
Expand Up @@ -2000,6 +2000,10 @@ type App struct {

// Cloud identifies the cloud instance the app represents.
Cloud string `yaml:"cloud,omitempty"`

// RequiredApps is a list of app names that are required for this app to function. Any app listed here will
// be part of the authentication redirect flow and authenticate along side this app.
RequiredApps []string `yaml:"required_apps,omitempty"`
}

// Rewrite is a list of rewriting rules to apply to requests and responses.
Expand Down
1 change: 1 addition & 0 deletions lib/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -5748,6 +5748,7 @@ func (process *TeleportProcess) initApps() {
Rewrite: rewrite,
AWS: aws,
Cloud: app.Cloud,
RequiredAppNames: app.RequiredAppNames,
})
if err != nil {
return trace.Wrap(err)
Expand Down
4 changes: 4 additions & 0 deletions lib/service/servicecfg/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ type App struct {

// Cloud identifies the cloud instance the app represents.
Cloud string

// RequiredAppNames is a list of app names that are required for this app to function. Any app listed here will
// be part of the authentication redirect flow and authenticate along side this app.
RequiredAppNames []string
}

// CheckAndSetDefaults validates an application.
Expand Down
4 changes: 2 additions & 2 deletions lib/web/apiserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -860,8 +860,8 @@ func (h *Handler) bindDefaultEndpoints() {
h.PUT("/webapi/trustedcluster/:name", h.WithAuth(h.upsertTrustedClusterHandle))
h.DELETE("/webapi/trustedcluster/:name", h.WithAuth(h.deleteTrustedCluster))

h.GET("/webapi/apps/:fqdnHint", h.WithAuth(h.getAppFQDN))
h.GET("/webapi/apps/:fqdnHint/:clusterName/:publicAddr", h.WithAuth(h.getAppFQDN))
h.GET("/webapi/apps/:fqdnHint", h.WithAuth(h.getAppDetails))
h.GET("/webapi/apps/:fqdnHint/:clusterName/:publicAddr", h.WithAuth(h.getAppDetails))

h.POST("/webapi/yaml/parse/:kind", h.WithAuth(h.yamlParse))
h.POST("/webapi/yaml/stringify/:kind", h.WithAuth(h.yamlStringify))
Expand Down
49 changes: 49 additions & 0 deletions lib/web/app/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,13 @@ type fragmentRequest struct {
StateValue string `json:"state_value"`
CookieValue string `json:"cookie_value"`
SubjectCookieValue string `json:"subject_cookie_value"`
RequiredApps string `json:"required_apps"`
}

// TeleportNextAppRedirectUrlHeader is used to tell the browser which URL to navigate to
// next in the chain of required app authentication redirects
const TeleportNextAppRedirectUrlHeader = "X-Teleport-NextAppRedirectUrl"

// startAppAuthExchange will do two actions depending on the following:
//
// 1): On initiating auth exchange (indicated by an empty "state" query param)
Expand All @@ -55,6 +60,28 @@ type fragmentRequest struct {
func (h *Handler) startAppAuthExchange(w http.ResponseWriter, r *http.Request, p httprouter.Params) error {
q := r.URL.Query()

requiredApps := strings.Split(q.Get("required-apps"), ",")
reqAddr, err := utils.ParseAddr(r.Host)
if err != nil {
return trace.Wrap(err)
}

// if required apps is length 1, it _should_ be itself as the final stop of the redirect chain.
// If that is the case, skip any further required app redirects and complete the auth flow.
// This value gets shifted off the front after a complete auth exchange
if len(requiredApps) > 1 && requiredApps[0] != reqAddr.Host() {
nextRequiredApp := requiredApps[0]

webLauncherURLParams := launcherURLParams{
clusterName: q.Get("cluster"),
publicAddr: nextRequiredApp,
arn: q.Get("arn"),
path: q.Get("path"),
requiredAppFQDNs: strings.Join(requiredApps, ","),
requiresAppRedirect: true,
}
return h.redirectToLauncher(w, r, webLauncherURLParams)
}
// Initiate auth exchange.
if q.Get("state") == "" {
// secretToken is the token we will look for in both the cookie
Expand Down Expand Up @@ -89,6 +116,8 @@ func (h *Handler) startAppAuthExchange(w http.ResponseWriter, r *http.Request, p
// - secretToken to compare against the one stored in cookie
// - cookieIdentifier to look up cookie sent by browser.
stateToken: fmt.Sprintf("%s_%s", secretToken, cookieIdentifier),
// required apps
requiredAppFQDNs: q.Get("required-apps"),
}
return h.redirectToLauncher(w, r, webLauncherURLParams)
}
Expand Down Expand Up @@ -195,6 +224,26 @@ func (h *Handler) completeAppAuthExchange(w http.ResponseWriter, r *http.Request
SameSite: http.SameSiteNoneMode,
})

requiredApps := strings.Split(req.RequiredApps, ",")
if len(requiredApps) > 1 {
requiredApps := requiredApps[1:]
nextRequiredApp := requiredApps[0]

webLauncherURLParams := launcherURLParams{
publicAddr: nextRequiredApp,
clusterName: h.clusterName,
requiredAppFQDNs: strings.Join(requiredApps, ","),
requiresAppRedirect: true,
}
addr, err := utils.ParseAddr(webLauncherURLParams.publicAddr)
if err != nil {
return trace.Wrap(err)
}
urlString := makeAppRedirectURL(r, h.c.WebPublicAddr, addr.Host(), webLauncherURLParams)
// this request does not return a response, so we can pass this value through a custom header instead
w.Header().Set(TeleportNextAppRedirectUrlHeader, urlString)
}

return nil
}

Expand Down
10 changes: 8 additions & 2 deletions lib/web/app/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -630,10 +630,13 @@ func makeAppRedirectURL(r *http.Request, proxyPublicAddr, hostname string, req l
}

// Presence of a stateToken means we are beginning an app auth exchange.
if req.stateToken != "" {
if req.stateToken != "" || req.requiresAppRedirect {
v := url.Values{}
v.Add("state", req.stateToken)
if req.stateToken != "" {
v.Add("state", req.stateToken)
}
v.Add("path", req.path)
v.Add("required-apps", req.requiredAppFQDNs)
u.RawQuery = v.Encode()

urlPath := []string{"web", "launch", hostname}
Expand Down Expand Up @@ -666,6 +669,9 @@ func makeAppRedirectURL(r *http.Request, proxyPublicAddr, hostname string, req l
// So Encode() will just encode it once (note that spaces will be convereted to `+`)
v := url.Values{}
v.Add("path", r.URL.Path)
if req.requiredAppFQDNs != "" {
v.Add("required-apps", req.requiredAppFQDNs)
}

if len(r.URL.RawQuery) > 0 {
v.Add("query", r.URL.RawQuery)
Expand Down
18 changes: 15 additions & 3 deletions lib/web/app/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -762,7 +762,7 @@ func TestMakeAppRedirectURL(t *testing.T) {
clusterName: "im-a-cluster-name",
publicAddr: "grafana.localhost",
},
expectedURL: "https://proxy.com/web/launch/grafana.localhost/im-a-cluster-name/grafana.localhost?path=&state=abc123",
expectedURL: "https://proxy.com/web/launch/grafana.localhost/im-a-cluster-name/grafana.localhost?path=&required-apps=&state=abc123",
},
{
name: "OK - with clusterId, publicAddr, and arn",
Expand All @@ -772,7 +772,7 @@ func TestMakeAppRedirectURL(t *testing.T) {
publicAddr: "grafana.localhost",
arn: "arn:aws:iam::123456789012:role%2Frole-name",
},
expectedURL: "https://proxy.com/web/launch/grafana.localhost/im-a-cluster-name/grafana.localhost/arn:aws:iam::123456789012:role%252Frole-name?path=&state=abc123",
expectedURL: "https://proxy.com/web/launch/grafana.localhost/im-a-cluster-name/grafana.localhost/arn:aws:iam::123456789012:role%252Frole-name?path=&required-apps=&state=abc123",
},
{
name: "OK - with clusterId, publicAddr, arn and path",
Expand All @@ -783,7 +783,19 @@ func TestMakeAppRedirectURL(t *testing.T) {
arn: "arn:aws:iam::123456789012:role%2Frole-name",
path: "/foo/bar?qux=qex",
},
expectedURL: "https://proxy.com/web/launch/grafana.localhost/im-a-cluster-name/grafana.localhost/arn:aws:iam::123456789012:role%252Frole-name?path=%2Ffoo%2Fbar%3Fqux%3Dqex&state=abc123",
expectedURL: "https://proxy.com/web/launch/grafana.localhost/im-a-cluster-name/grafana.localhost/arn:aws:iam::123456789012:role%252Frole-name?path=%2Ffoo%2Fbar%3Fqux%3Dqex&required-apps=&state=abc123",
},
{
name: "OK - with clusterId, publicAddr, arn, path, and required-apps",
launderURLParams: launcherURLParams{
stateToken: "abc123",
clusterName: "im-a-cluster-name",
publicAddr: "grafana.localhost",
arn: "arn:aws:iam::123456789012:role%2Frole-name",
path: "/foo/bar?qux=qex",
requiredAppFQDNs: "api.example.com,grafana.localhost",
},
expectedURL: "https://proxy.com/web/launch/grafana.localhost/im-a-cluster-name/grafana.localhost/arn:aws:iam::123456789012:role%252Frole-name?path=%2Ffoo%2Fbar%3Fqux%3Dqex&required-apps=api.example.com%2Cgrafana.localhost&state=abc123",
},
} {
t.Run(test.name, func(t *testing.T) {
Expand Down
15 changes: 13 additions & 2 deletions lib/web/app/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func (h *Handler) withAuth(handler handlerAuthFunc) http.HandlerFunc {
// redirectToLauncher redirects to the proxy web's app launcher if the public
// address of the proxy is set.
func (h *Handler) redirectToLauncher(w http.ResponseWriter, r *http.Request, p launcherURLParams) error {
if p.stateToken == "" && !HasSessionCookie(r) {
if p.stateToken == "" && !HasSessionCookie(r) && !p.requiresAppRedirect {
// Reaching this block means the application was accessed through the CLI (eg: tsh app login)
// and there was a forwarding error and we could not renew the app web session.
// Since we can't redirect the user to the app launcher from the CLI,
Expand All @@ -81,7 +81,12 @@ func (h *Handler) redirectToLauncher(w http.ResponseWriter, r *http.Request, p l
"https://goteleport.com/docs/application-access/guides/connecting-apps/#start-authproxy-service.")
return trace.BadParameter("public address of the proxy is not set")
}
addr, err := utils.ParseAddr(r.Host)
host := p.publicAddr
if host == "" {
host = r.Host
}

addr, err := utils.ParseAddr(host)
if err != nil {
return trace.Wrap(err)
}
Expand Down Expand Up @@ -139,4 +144,10 @@ type launcherURLParams struct {
// This field is used to preserve the original requested path through
// the app access authentication redirections.
path string
// requiredAppFQDNs is a list of required app fqdn to be used during application
// authentication redirects.
requiredAppFQDNs string
// requiredAppRedirect is used to tell the url builder an app redirect is required. If required,
// it will build the full launcher redirect url (similar to the one built with the stateToken)
requiresAppRedirect bool
}
Loading

0 comments on commit db6523e

Please sign in to comment.