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
2 changes: 1 addition & 1 deletion e
Submodule e updated from e0341f to 7789b0
143 changes: 0 additions & 143 deletions lib/web/apiserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -559,17 +559,6 @@ func (h *Handler) bindDefaultEndpoints(challengeLimiter *limiter.RateLimiter) {
// Kube access handlers.
h.GET("/webapi/sites/:site/kubernetes", h.WithClusterAuth(h.clusterKubesGet))

// OIDC related callback handlers
h.GET("/webapi/oidc/login/web", h.WithRedirect(h.oidcLoginWeb))
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.WithMetaRedirect(h.samlACS))
h.POST("/webapi/saml/acs/:connector", h.WithMetaRedirect(h.samlACS))
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.WithMetaRedirect(h.githubCallback))
Expand Down Expand Up @@ -1207,32 +1196,6 @@ func (h *Handler) motd(w http.ResponseWriter, r *http.Request, p httprouter.Para
return webclient.MotD{Text: authPrefs.GetMessageOfTheDay()}, nil
}

func (h *Handler) oidcLoginWeb(w http.ResponseWriter, r *http.Request, p httprouter.Params) string {
logger := h.log.WithField("auth", "oidc")
logger.Debug("Web login start.")

req, err := ParseSSORequestParams(r)
if err != nil {
logger.WithError(err).Error("Failed to extract SSO parameters from request.")
return client.LoginFailedRedirectURL
}

response, err := h.cfg.ProxyClient.CreateOIDCAuthRequest(r.Context(), types.OIDCAuthRequest{
CSRFToken: req.CSRFToken,
ConnectorID: req.ConnectorID,
CreateWebSession: true,
ClientRedirectURL: req.ClientRedirectURL,
CheckUser: true,
ProxyAddress: r.Host,
})
if err != nil {
logger.WithError(err).Error("Error creating auth request.")
return client.LoginFailedRedirectURL
}

return response.RedirectURL
}

func (h *Handler) githubLoginWeb(w http.ResponseWriter, r *http.Request, p httprouter.Params) string {
logger := h.log.WithField("auth", "github")
logger.Debug("Web login start.")
Expand Down Expand Up @@ -1362,112 +1325,6 @@ func (h *Handler) githubCallback(w http.ResponseWriter, r *http.Request, p httpr
return redirectURL.String()
}

func (h *Handler) oidcLoginConsole(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
logger := h.log.WithField("auth", "oidc")
logger.Debug("Console login start.")

req := new(client.SSOLoginConsoleReq)
if err := httplib.ReadJSON(r, req); err != nil {
logger.WithError(err).Error("Error reading json.")
return nil, trace.AccessDenied(SSOLoginFailureMessage)
}

if err := req.CheckAndSetDefaults(); err != nil {
logger.WithError(err).Error("Missing request parameters.")
return nil, trace.AccessDenied(SSOLoginFailureMessage)
}

response, err := h.cfg.ProxyClient.CreateOIDCAuthRequest(r.Context(), types.OIDCAuthRequest{
ConnectorID: req.ConnectorID,
ClientRedirectURL: req.RedirectURL,
PublicKey: req.PublicKey,
CertTTL: req.CertTTL,
CheckUser: true,
Compatibility: req.Compatibility,
RouteToCluster: req.RouteToCluster,
KubernetesCluster: req.KubernetesCluster,
ProxyAddress: r.Host,
AttestationStatement: req.AttestationStatement.ToProto(),
})
if err != nil {
logger.WithError(err).Error("Failed to create OIDC auth request.")
return nil, trace.AccessDenied(SSOLoginFailureMessage)
}

return &client.SSOLoginConsoleResponse{
RedirectURL: response.RedirectURL,
}, nil
}

func (h *Handler) oidcCallback(w http.ResponseWriter, r *http.Request, p httprouter.Params) string {
logger := h.log.WithField("auth", "oidc")
logger.Debug("Callback start.")

response, err := h.cfg.ProxyClient.ValidateOIDCAuthCallback(r.Context(), r.URL.Query())
if err != nil {
logger.WithError(err).Error("Error while processing callback.")

// try to find the auth request, which bears the original client redirect URL.
// if found, use it to terminate the flow.
//
// this improves the UX by terminating the failed SSO flow immediately, rather than hoping for a timeout.
if requestID := r.URL.Query().Get("state"); requestID != "" {
if request, errGet := h.cfg.ProxyClient.GetOIDCAuthRequest(r.Context(), requestID); errGet == nil && !request.CreateWebSession {
if redURL, errEnc := RedirectURLWithError(request.ClientRedirectURL, err); errEnc == nil {
return redURL.String()
}
}
}

if errors.Is(err, auth.ErrOIDCNoRoles) {
return client.LoginFailedUnauthorizedRedirectURL
}

return client.LoginFailedBadCallbackRedirectURL
}

// if we created web session, set session cookie and redirect to original url
if response.Req.CreateWebSession {
logger.Info("Redirecting to web browser.")

res := &SSOCallbackResponse{
CSRFToken: response.Req.CSRFToken,
Username: response.Username,
SessionName: response.Session.GetName(),
ClientRedirectURL: response.Req.ClientRedirectURL,
}

if err := SSOSetWebSessionAndRedirectURL(w, r, res, true); err != nil {
logger.WithError(err).Error("Error setting web session.")
return client.LoginFailedRedirectURL
}

return res.ClientRedirectURL
}

logger.Info("Callback redirecting to console login.")
if len(response.Req.PublicKey) == 0 {
logger.Error("Not a web or console login request.")
return client.LoginFailedRedirectURL
}

redirectURL, err := ConstructSSHResponse(AuthParams{
ClientRedirectURL: response.Req.ClientRedirectURL,
Username: response.Username,
Identity: response.Identity,
Session: response.Session,
Cert: response.Cert,
TLSCert: response.TLSCert,
HostSigners: response.HostSigners,
})
if err != nil {
logger.WithError(err).Error("Error constructing ssh response")
return client.LoginFailedRedirectURL
}

return redirectURL.String()
}

func (h *Handler) installer(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
httplib.SetScriptHeaders(w.Header())

Expand Down
151 changes: 0 additions & 151 deletions lib/web/apiserver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,10 @@ import (
"archive/tar"
"bufio"
"bytes"
"compress/flate"
"compress/gzip"
"context"
"crypto/tls"
"encoding/base32"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
Expand All @@ -40,14 +38,12 @@ import (
"os"
"os/user"
"path/filepath"
"regexp"
"sort"
"strings"
"sync"
"testing"
"time"

"github.com/beevik/etree"
"github.com/gogo/protobuf/proto"
"github.com/google/go-cmp/cmp"
"github.com/google/uuid"
Expand All @@ -67,13 +63,11 @@ import (
"github.com/gravitational/teleport/lib/auth/native"
"github.com/gravitational/teleport/lib/auth/testauthority"
wanlib "github.com/gravitational/teleport/lib/auth/webauthn"
"github.com/gravitational/teleport/lib/backend"
"github.com/gravitational/teleport/lib/bpf"
"github.com/gravitational/teleport/lib/client"
"github.com/gravitational/teleport/lib/client/conntest"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/events"
"github.com/gravitational/teleport/lib/fixtures"
"github.com/gravitational/teleport/lib/httplib"
"github.com/gravitational/teleport/lib/httplib/csrf"
kubeproxy "github.com/gravitational/teleport/lib/kube/proxy"
Expand Down Expand Up @@ -108,7 +102,6 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
kyaml "k8s.io/apimachinery/pkg/util/yaml"
authztypes "k8s.io/client-go/kubernetes/typed/authorization/v1"
"k8s.io/client-go/tools/clientcmd"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
Expand Down Expand Up @@ -573,137 +566,6 @@ func Test_clientMetaFromReq(t *testing.T) {
}, got)
}

func TestSAML(t *testing.T) {
t.Parallel()

tests := []struct {
name string
rawConnector string
validSession bool
expectedRedirectURL string
}{
{
name: "success",
rawConnector: fixtures.SAMLOktaConnectorV2,
validSession: true,
expectedRedirectURL: "/after",
},
{
name: "fail to map claims to roles",
rawConnector: strings.ReplaceAll(fixtures.SAMLOktaConnectorV2, "Everyone", "No-one"),
validSession: false,
expectedRedirectURL: client.LoginFailedUnauthorizedRedirectURL,
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
ctx := context.Background()
s := newWebSuite(t)
input := tc.rawConnector

decoder := kyaml.NewYAMLOrJSONDecoder(strings.NewReader(input), defaults.LookaheadBufSize)
var raw services.UnknownResource
err := decoder.Decode(&raw)
require.NoError(t, err)

connector, err := services.UnmarshalSAMLConnector(raw.Raw)
require.NoError(t, err)

role, err := types.NewRoleV3(connector.GetAttributesToRoles()[0].Roles[0], types.RoleSpecV5{
Options: types.RoleOptions{
MaxSessionTTL: types.NewDuration(apidefaults.MaxCertDuration),
},
Allow: types.RoleConditions{
NodeLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
Namespaces: []string{apidefaults.Namespace},
Rules: []types.Rule{
types.NewRule(types.Wildcard, services.RW()),
},
},
})
require.NoError(t, err)
role.SetLogins(types.Allow, []string{s.user})
err = s.server.Auth().UpsertRole(s.ctx, role)
require.NoError(t, err)

err = s.server.Auth().UpsertSAMLConnector(ctx, connector)
require.NoError(t, err)
s.server.Auth().SetClock(clockwork.NewFakeClockAt(time.Date(2017, 5, 10, 18, 53, 0, 0, time.UTC)))
clt := s.clientNoRedirects()

csrfToken := "2ebcb768d0090ea4368e42880c970b61865c326172a4a2343b645cf5d7f20992"

baseURL, err := url.Parse(clt.Endpoint("webapi", "saml", "sso") + `?connector_id=` + connector.GetName() + `&redirect_url=http://localhost/after`)
require.NoError(t, err)
req, err := http.NewRequest("GET", baseURL.String(), nil)
require.NoError(t, err)
addCSRFCookieToReq(req, csrfToken)
re, err := clt.Client.RoundTrip(func() (*http.Response, error) {
return clt.Client.HTTPClient().Do(req)
})
require.NoError(t, err)

// we got a redirect
urlPattern := regexp.MustCompile(`URL='([^']*)'`)
locationURL := urlPattern.FindStringSubmatch(string(re.Bytes()))[1]
u, err := url.Parse(locationURL)
require.NoError(t, err)
require.Equal(t, fixtures.SAMLOktaSSO, u.Scheme+"://"+u.Host+u.Path)
data, err := base64.StdEncoding.DecodeString(u.Query().Get("SAMLRequest"))
require.NoError(t, err)
buf, err := io.ReadAll(flate.NewReader(bytes.NewReader(data)))
require.NoError(t, err)
doc := etree.NewDocument()
err = doc.ReadFromBytes(buf)
require.NoError(t, err)
id := doc.Root().SelectAttr("ID")
require.NotNil(t, id)

authRequest, err := s.server.Auth().GetSAMLAuthRequest(context.Background(), id.Value)
require.NoError(t, err)

// now swap the request id to the hardcoded one in fixtures
authRequest.ID = fixtures.SAMLOktaAuthRequestID
authRequest.CSRFToken = csrfToken
err = s.server.Auth().Services.CreateSAMLAuthRequest(ctx, *authRequest, backend.Forever)
require.NoError(t, err)

// now respond with pre-recorded request to the POST url
in := &bytes.Buffer{}
fw, err := flate.NewWriter(in, flate.DefaultCompression)
require.NoError(t, err)

_, err = fw.Write([]byte(fixtures.SAMLOktaAuthnResponseXML))
require.NoError(t, err)
err = fw.Close()
require.NoError(t, err)
encodedResponse := base64.StdEncoding.EncodeToString(in.Bytes())
require.NotNil(t, encodedResponse)

// now send the response to the server to exchange it for auth session
form := url.Values{}
form.Add("SAMLResponse", encodedResponse)
req, err = http.NewRequest("POST", clt.Endpoint("webapi", "saml", "acs"), strings.NewReader(form.Encode()))
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
addCSRFCookieToReq(req, csrfToken)
require.NoError(t, err)
authRe, err := clt.Client.RoundTrip(func() (*http.Response, error) {
return clt.Client.HTTPClient().Do(req)
})

require.NoError(t, err)
// This route uses a meta redirect, so expect redirect URL in body instead of location header.
require.Equal(t, http.StatusOK, authRe.Code(), "Response: %v", string(authRe.Bytes()))
if tc.validSession {
// we have got valid session
require.NotEmpty(t, authRe.Headers().Get("Set-Cookie"))
}
require.Contains(t, string(authRe.Bytes()), tc.expectedRedirectURL)
})
}
}

func TestWebSessionsCRUD(t *testing.T) {
t.Parallel()
s := newWebSuite(t)
Expand Down Expand Up @@ -4852,19 +4714,6 @@ func (s *WebSuite) listenForResizeEvent(ws *websocket.Conn) chan struct{} {
return ch
}

func (s *WebSuite) clientNoRedirects(opts ...roundtrip.ClientParam) *client.WebClient {
hclient := client.NewInsecureWebClient()
hclient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
opts = append(opts, roundtrip.HTTPClient(hclient))
wc, err := client.NewWebClient(s.url().String(), opts...)
if err != nil {
panic(err)
}
return wc
}

func (s *WebSuite) client(opts ...roundtrip.ClientParam) *client.WebClient {
opts = append(opts, roundtrip.HTTPClient(client.NewInsecureWebClient()))
wc, err := client.NewWebClient(s.url().String(), opts...)
Expand Down
Loading