Skip to content

Commit

Permalink
Add meta redirect (#8980)
Browse files Browse the repository at this point in the history
Co-authored-by: Russell Jones <[email protected]>
  • Loading branch information
atburke and russjones authored Nov 18, 2021
1 parent f5ce857 commit 4d574ea
Show file tree
Hide file tree
Showing 4 changed files with 73 additions and 8 deletions.
46 changes: 43 additions & 3 deletions lib/web/apiserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,21 @@ import (
const (
// ssoLoginConsoleErr is a generic error message to hide revealing sso login failure msgs.
ssoLoginConsoleErr = "Failed to login. Please check Teleport's log for more details."
metaRedirectHTML = `
<!DOCTYPE html>
<html lang="en">
<head>
<title>Teleport Redirection Service</title>
<meta http-equiv="cache-control" content="no-cache"/>
<meta http-equiv="refresh" content="0;URL='{{.}}'" />
</head>
<body></body>
</html>
`
)

var metaRedirectTemplate = template.Must(template.New("meta-redirect").Parse(metaRedirectHTML))

// Handler is HTTP web proxy handler
type Handler struct {
log logrus.FieldLogger
Expand Down Expand Up @@ -332,17 +345,17 @@ func NewHandler(cfg Config, opts ...HandlerOption) (*RewritingHandler, error) {

// OIDC related callback handlers
h.GET("/webapi/oidc/login/web", h.WithRedirect(h.oidcLoginWeb))
h.GET("/webapi/oidc/callback", h.WithRedirect(h.oidcCallback))
h.GET("/webapi/oidc/callback", h.WithMetaRedirect(h.oidcCallback))
h.POST("/webapi/oidc/login/console", httplib.MakeHandler(h.oidcLoginConsole))

// SAML 2.0 handlers
h.POST("/webapi/saml/acs", h.WithRedirect(h.samlACS))
h.GET("/webapi/saml/sso", h.WithRedirect(h.samlSSO))
h.GET("/webapi/saml/sso", h.WithMetaRedirect(h.samlSSO))
h.POST("/webapi/saml/login/console", httplib.MakeHandler(h.samlSSOConsole))

// Github connector handlers
h.GET("/webapi/github/login/web", h.WithRedirect(h.githubLoginWeb))
h.GET("/webapi/github/callback", h.WithRedirect(h.githubCallback))
h.GET("/webapi/github/callback", h.WithMetaRedirect(h.githubCallback))
h.POST("/webapi/github/login/console", httplib.MakeHandler(h.githubLoginConsole))

// U2F related APIs
Expand Down Expand Up @@ -2490,17 +2503,44 @@ func (h *Handler) WithClusterAuth(fn ClusterHandler) httprouter.Handle {

type redirectHandlerFunc func(w http.ResponseWriter, r *http.Request, p httprouter.Params) (redirectURL string)

func isValidRedirectURL(redirectURL string) bool {
u, err := url.ParseRequestURI(redirectURL)
return err == nil && (!u.IsAbs() || (u.Scheme == "http" || u.Scheme == "https"))
}

// WithRedirect is a handler that redirects to the path specified in the returned value.
func (h *Handler) WithRedirect(fn redirectHandlerFunc) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
// ensure that neither proxies nor browsers cache http traffic
httplib.SetNoCacheHeaders(w.Header())

redirectURL := fn(w, r, p)
if !isValidRedirectURL(redirectURL) {
redirectURL = client.LoginFailedRedirectURL
}
http.Redirect(w, r, redirectURL, http.StatusFound)
}
}

// WithMetaRedirect is a handler that redirects to the path specified
// using HTML rather than HTTP. This is needed for redirects that can
// have a header size larger than 8kb, which some middlewares will drop.
// See https://github.com/gravitational/teleport/issues/7467.
func (h *Handler) WithMetaRedirect(fn redirectHandlerFunc) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
httplib.SetNoCacheHeaders(w.Header())
app.SetRedirectPageHeaders(w.Header(), "")
redirectURL := fn(w, r, p)
if !isValidRedirectURL(redirectURL) {
redirectURL = client.LoginFailedRedirectURL
}
err := metaRedirectTemplate.Execute(w, redirectURL)
if err != nil {
h.log.WithError(err).Warn("Failed to execute template.")
}
}
}

// WithAuth ensures that request is authenticated
func (h *Handler) WithAuth(fn ContextHandler) httprouter.Handle {
return httplib.MakeHandler(func(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
Expand Down
23 changes: 22 additions & 1 deletion lib/web/apiserver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import (
"net/url"
"os"
"os/user"
"regexp"
"sort"
"strings"
"testing"
Expand Down Expand Up @@ -477,6 +478,25 @@ func (s *WebSuite) createUser(c *C, user string, login string, pass string, otpS
}
}

func TestValidRedirectURL(t *testing.T) {
t.Parallel()
for _, tt := range []struct {
desc, url string
valid bool
}{
{"valid absolute https url", "https://example.com?a=1", true},
{"valid absolute http url", "http://example.com?a=1", true},
{"valid relative url", "/path/to/something", true},
{"garbage", "fjoiewjwpods302j09", false},
{"empty string", "", false},
{"block bad protocol", "javascript:alert('xss')", false},
} {
t.Run(tt.desc, func(t *testing.T) {
require.Equal(t, tt.valid, isValidRedirectURL(tt.url))
})
}
}

func (s *WebSuite) TestSAMLSuccess(c *C) {
input := fixtures.SAMLOktaConnectorV2

Expand Down Expand Up @@ -525,7 +545,8 @@ func (s *WebSuite) TestSAMLSuccess(c *C) {
c.Assert(err, IsNil)

// we got a redirect
locationURL := re.Headers().Get("Location")
urlPattern := regexp.MustCompile(`URL='([^']*)'`)
locationURL := urlPattern.FindStringSubmatch(string(re.Bytes()))[1]
u, err := url.Parse(locationURL)
c.Assert(err, IsNil)
c.Assert(u.Scheme+"://"+u.Host+u.Path, Equals, fixtures.SAMLOktaSSO)
Expand Down
2 changes: 1 addition & 1 deletion lib/web/app/fragment.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ func (h *Handler) handleFragment(w http.ResponseWriter, r *http.Request, p httpr
h.log.WithError(err).Debugf("Failed to generate and encode random numbers.")
return trace.AccessDenied("access denied")
}
setRedirectPageHeaders(w.Header(), nonce)
SetRedirectPageHeaders(w.Header(), nonce)
fmt.Fprintf(w, js, nonce)
return nil

Expand Down
10 changes: 7 additions & 3 deletions lib/web/app/redirect.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,16 @@ import (
"github.com/gravitational/teleport/lib/httplib"
)

func setRedirectPageHeaders(h http.Header, nonce string) {
func SetRedirectPageHeaders(h http.Header, nonce string) {
httplib.SetIndexHTMLHeaders(h)
// Set content policy flags
var csp = strings.Join([]string{
scriptSrc := "none"
if nonce != "" {
// Should match the <script> tab nonce (random value).
fmt.Sprintf("script-src 'nonce-%v'", nonce),
scriptSrc = fmt.Sprintf("nonce-%v", nonce)
}
var csp = strings.Join([]string{
fmt.Sprintf("script-src '%v'", scriptSrc),
"style-src 'self'",
"object-src 'none'",
"img-src 'self'",
Expand Down

0 comments on commit 4d574ea

Please sign in to comment.