Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
5 changes: 5 additions & 0 deletions api/breaker/round_tripper.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,8 @@ func (t *RoundTripper) RoundTrip(request *http.Request) (*http.Response, error)

return v.(*http.Response), err
}

// Unwrap returns the inner round tripper.
func (t *RoundTripper) Unwrap() http.RoundTripper {
return t.tripper
}
51 changes: 25 additions & 26 deletions api/observability/tracing/http/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package http

import (
"net/http"
nethttp "net/http"

"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
Expand Down Expand Up @@ -43,39 +44,37 @@ func HandlerFormatter(operation string, r *nethttp.Request) string {
// https://github.com/open-telemetry/opentelemetry-go-contrib/issues/3543.
// Once the issue is resolved the wrapper may be discarded.
func NewTransport(rt nethttp.RoundTripper) nethttp.RoundTripper {
return enforceCloseIdleConnections(
otelhttp.NewTransport(rt,
otelhttp.WithSpanNameFormatter(TransportFormatter),
), rt)
return &roundTripWrapper{
RoundTripper: otelhttp.NewTransport(rt, otelhttp.WithSpanNameFormatter(TransportFormatter)),
inner: rt,
}
}

type closeIdler interface {
CloseIdleConnections()
}

type roundTripWrapper struct {
nethttp.RoundTripper
inner nethttp.RoundTripper
}

// Unwrap returns the inner round tripper.
func (w *roundTripWrapper) Unwrap() http.RoundTripper {
return w.inner
}

// enforceCloseIdleConnections ensures that the returned [nethttp.RoundTripper]
// CloseIdleConnections ensures that the returned [nethttp.RoundTripper]
// has a CloseIdleConnections method. Since [otelhttp.Transport] does not implement
// this any [nethttp.Client.CloseIdleConnections] calls result in a noop instead
// of forwarding the request onto its wrapped [nethttp.RoundTripper].
//
// DELETE WHEN https://github.com/open-telemetry/opentelemetry-go-contrib/issues/3543
// has been resolved.
func enforceCloseIdleConnections(wrapper, wrapped nethttp.RoundTripper) nethttp.RoundTripper {
type closeIdler interface {
CloseIdleConnections()
func (w *roundTripWrapper) CloseIdleConnections() {
if c, ok := w.RoundTripper.(closeIdler); ok {
c.CloseIdleConnections()
} else if c, ok := w.inner.(closeIdler); ok {
c.CloseIdleConnections()
}

type unwrapper struct {
nethttp.RoundTripper
closeIdler
}

if _, ok := wrapper.(closeIdler); ok {
return wrapper
}

if c, ok := wrapped.(closeIdler); ok {
return &unwrapper{
RoundTripper: wrapper,
closeIdler: c,
}
}

return wrapper
}
2 changes: 1 addition & 1 deletion e
Submodule e updated from f64397 to 76f403
28 changes: 28 additions & 0 deletions lib/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -4719,6 +4719,34 @@ func (a *Server) GetLicense(ctx context.Context) (string, error) {
return fmt.Sprintf("%s%s", a.license.CertPEM, a.license.KeyPEM), nil
}

// GetHeadlessAuthentication returns a headless authentication from the backend by name.
// If it does not yet exist, a stub will be created to signal the login process to upsert
// login details. This method will wait for the updated headless authentication and return it.
func (a *Server) GetHeadlessAuthentication(ctx context.Context, name string) (*types.HeadlessAuthentication, error) {
// Try to create a stub if it doesn't already exist, then wait for full login details.
if _, err := a.Services.CreateHeadlessAuthenticationStub(ctx, name); err != nil && !trace.IsAlreadyExists(err) {
return nil, trace.Wrap(err)
}

// wait for the headless authentication to be updated with valid login details
// by the login process. If the headless authentication is already updated,
// Wait will return it immediately.
waitCtx, cancel := context.WithTimeout(ctx, defaults.HTTPRequestTimeout)
defer cancel()

headlessAuthn, err := a.headlessAuthenticationWatcher.Wait(waitCtx, name, func(ha *types.HeadlessAuthentication) (bool, error) {
return services.ValidateHeadlessAuthentication(ha) == nil, nil
})
return headlessAuthn, trace.Wrap(err)
}

// CompareAndSwapHeadlessAuthentication performs a compare
// and swap replacement on a headless authentication resource.
func (a *Server) CompareAndSwapHeadlessAuthentication(ctx context.Context, old, new *types.HeadlessAuthentication) (*types.HeadlessAuthentication, error) {
headlessAuthn, err := a.Services.CompareAndSwapHeadlessAuthentication(ctx, old, new)
return headlessAuthn, trace.Wrap(err)
}

// authKeepAliver is a keep aliver using auth server directly
type authKeepAliver struct {
sync.RWMutex
Expand Down
109 changes: 109 additions & 0 deletions lib/auth/auth_login_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"github.com/gravitational/teleport/lib/auth/mocku2f"
wanlib "github.com/gravitational/teleport/lib/auth/webauthn"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/services"
)

func TestServer_CreateAuthenticateChallenge_authPreference(t *testing.T) {
Expand Down Expand Up @@ -701,6 +702,114 @@ func TestServer_Authenticate_nonPasswordlessRequiresUsername(t *testing.T) {
}
}

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

ctx := context.Background()
headlessID := services.NewHeadlessAuthenticationID([]byte(sshPubKey))
const timeout = time.Millisecond * 200

type updateHeadlessAuthnFn func(*types.HeadlessAuthentication, *types.MFADevice)
updateHeadlessAuthnInGoroutine := func(ctx context.Context, srv *TestTLSServer, mfa *types.MFADevice, update updateHeadlessAuthnFn) chan error {
errC := make(chan error)
go func() {
defer close(errC)

headlessAuthn, err := srv.Auth().GetHeadlessAuthentication(ctx, headlessID)
if err != nil {
errC <- err
return
}

// create a shallow copy and update for the compare and swap below.
replaceHeadlessAuthn := *headlessAuthn
update(&replaceHeadlessAuthn, mfa)

_, err = srv.Auth().CompareAndSwapHeadlessAuthentication(ctx, headlessAuthn, &replaceHeadlessAuthn)
if err != nil {
errC <- err
return
}
}()
return errC
}

for _, tc := range []struct {
name string
update updateHeadlessAuthnFn
checkErr require.ErrorAssertionFunc
}{
{
name: "OK approved",
update: func(ha *types.HeadlessAuthentication, mfa *types.MFADevice) {
ha.State = types.HeadlessAuthenticationState_HEADLESS_AUTHENTICATION_STATE_APPROVED
ha.MfaDevice = mfa
},
checkErr: require.NoError,
}, {
name: "NOK approved without MFA",
update: func(ha *types.HeadlessAuthentication, mfa *types.MFADevice) {
ha.State = types.HeadlessAuthenticationState_HEADLESS_AUTHENTICATION_STATE_APPROVED
},
checkErr: require.Error,
}, {
name: "NOK user mismatch",
update: func(ha *types.HeadlessAuthentication, mfa *types.MFADevice) {
ha.State = types.HeadlessAuthenticationState_HEADLESS_AUTHENTICATION_STATE_APPROVED
ha.MfaDevice = mfa
ha.User = "other-user"
},
checkErr: require.Error,
}, {
name: "NOK denied",
update: func(ha *types.HeadlessAuthentication, mfa *types.MFADevice) {
ha.State = types.HeadlessAuthenticationState_HEADLESS_AUTHENTICATION_STATE_DENIED
},
checkErr: require.Error,
}, {
name: "NOK timeout",
update: func(ha *types.HeadlessAuthentication, mfa *types.MFADevice) {},
checkErr: require.Error,
},
} {
t.Run(tc.name, func(t *testing.T) {
tc := tc
t.Parallel()

srv := newTestTLSServer(t)
proxyClient, err := srv.NewClient(TestBuiltin(types.RoleProxy))
require.NoError(t, err)

// We don't mind about the specifics of the configuration, as long as we have
// a user and TOTP/WebAuthn devices.
mfa := configureForMFA(t, srv)
username := mfa.User

t.Cleanup(func() {
srv.Auth().DeleteHeadlessAuthentication(ctx, headlessID)
})
Comment thread
Joerger marked this conversation as resolved.
Outdated

ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()

errC := updateHeadlessAuthnInGoroutine(ctx, srv, mfa.WebDev.MFA, tc.update)
_, err = proxyClient.AuthenticateSSHUser(ctx, AuthenticateSSHRequest{
AuthenticateUserRequest: AuthenticateUserRequest{
Username: username,
PublicKey: []byte(sshPubKey),
HeadlessAuthenticationID: headlessID,
ClientMetadata: &ForwardedClientMetadata{
RemoteAddr: "0.0.0.0",
},
},
TTL: defaults.CallbackTimeout,
})
tc.checkErr(t, err)
require.NoError(t, <-errC)
})
}
}

type configureMFAResp struct {
User, Password string
TOTPDev, WebDev *TestDevice
Expand Down
105 changes: 77 additions & 28 deletions lib/auth/auth_with_roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"strings"
"time"

"github.com/gravitational/roundtrip"
"github.com/gravitational/trace"
"github.com/sirupsen/logrus"
collectortracev1 "go.opentelemetry.io/proto/otlp/collector/trace/v1"
Expand Down Expand Up @@ -1119,30 +1120,6 @@ func (a *ServerWithRoles) UpsertNode(ctx context.Context, s types.Server) (*type
return a.authServer.UpsertNode(ctx, s)
}

// DELETE IN: 5.1.0
//
// This logic has moved to KeepAliveServer.
func (a *ServerWithRoles) KeepAliveNode(ctx context.Context, handle types.KeepAlive) error {
if !a.hasBuiltinRole(types.RoleNode) {
return trace.AccessDenied("[10] access denied")
}
clusterName, err := a.GetDomainName(ctx)
if err != nil {
return trace.Wrap(err)
}
serverName, err := ExtractHostID(a.context.User.GetName(), clusterName)
if err != nil {
return trace.AccessDenied("[10] access denied")
}
if serverName != handle.Name {
return trace.AccessDenied("[10] access denied")
}
if err := a.action(apidefaults.Namespace, types.KindNode, types.VerbUpdate); err != nil {
return trace.Wrap(err)
}
return a.authServer.KeepAliveNode(ctx, handle)
}

// KeepAliveServer updates expiry time of a server resource.
func (a *ServerWithRoles) KeepAliveServer(ctx context.Context, handle types.KeepAlive) error {
clusterName, err := a.GetDomainName(ctx)
Expand Down Expand Up @@ -5732,14 +5709,86 @@ func (a *ServerWithRoles) DeleteAllUserGroups(ctx context.Context) error {

// GetHeadlessAuthentication retrieves a headless authentication by id.
func (a *ServerWithRoles) GetHeadlessAuthentication(ctx context.Context, id string) (*types.HeadlessAuthentication, error) {
// TODO (joerger): Add implementation - follow up PR
return nil, trace.NotImplemented("GetHeadlessAuthentication is not implemented")
// GetHeadlessAuthentication will wait for the headless details
// if they don't yet exist in the backend.
ctx, cancel := context.WithTimeout(ctx, defaults.HTTPRequestTimeout)
defer cancel()

headlessAuthn, err := a.authServer.GetHeadlessAuthentication(ctx, id)
if err != nil {
return nil, trace.Wrap(err)
}

// Only users can get their own headless authentication requests.
if !hasLocalUserRole(a.context) || headlessAuthn.User != a.context.User.GetName() {
// This method would usually time out above if the headless authentication
// does not exist, so we mimick this behavior here for users without access.
<-ctx.Done()
return nil, trace.Wrap(ctx.Err())
}

return headlessAuthn, nil
}

// UpdateHeadlessAuthenticationState updates a headless authentication state.
func (a *ServerWithRoles) UpdateHeadlessAuthenticationState(ctx context.Context, id string, state types.HeadlessAuthenticationState, mfaResp *proto.MFAAuthenticateResponse) error {
// TODO (joerger): Add implementation - follow up PR
return trace.NotImplemented("UpdateHeadlessAuthenticationState is not implemented")
// GetHeadlessAuthentication will wait for the headless details
// if they don't yet exist in the backend.
ctx, cancel := context.WithTimeout(ctx, defaults.HTTPRequestTimeout)
defer cancel()

headlessAuthn, err := a.authServer.GetHeadlessAuthentication(ctx, id)
if err != nil {
return trace.Wrap(err)
}

// Only users can update their own headless authentication requests.
if !hasLocalUserRole(a.context) || headlessAuthn.User != a.context.User.GetName() {
// This method would usually time out above if the headless authentication
// does not exist, so we mimick this behavior here for users without access.
<-ctx.Done()
return trace.Wrap(ctx.Err())
}

if headlessAuthn.State != types.HeadlessAuthenticationState_HEADLESS_AUTHENTICATION_STATE_PENDING {
return trace.AccessDenied("cannot update a headless authentication state from a non-pending state")
}

// Shallow copy headless authn for compare and swap below.
replaceHeadlessAuthn := *headlessAuthn
replaceHeadlessAuthn.State = state

switch state {
case types.HeadlessAuthenticationState_HEADLESS_AUTHENTICATION_STATE_APPROVED:
// The user must authenticate with MFA to change the state to approved.
if mfaResp == nil {
return trace.BadParameter("expected MFA auth challenge response")
}

// Only WebAuthn is supported in headless login flow for superior phishing prevention.
if _, ok := mfaResp.Response.(*proto.MFAAuthenticateResponse_Webauthn); !ok {
return trace.BadParameter("expected WebAuthn challenge response, but got %T", mfaResp.Response)
}

mfaDevice, _, err := a.authServer.validateMFAAuthResponse(ctx, mfaResp, headlessAuthn.User, false /* passwordless */)
if err != nil {
return trace.Wrap(err)
}

replaceHeadlessAuthn.MfaDevice = mfaDevice
case types.HeadlessAuthenticationState_HEADLESS_AUTHENTICATION_STATE_DENIED:
// continue to compare and swap without MFA.
default:
return trace.AccessDenied("cannot update a headless authentication state to %v", state.String())
}

_, err = a.authServer.CompareAndSwapHeadlessAuthentication(ctx, headlessAuthn, &replaceHeadlessAuthn)
return trace.Wrap(err)
}

// CloneHTTPClient creates a new HTTP client with the same configuration.
func (a *ServerWithRoles) CloneHTTPClient(params ...roundtrip.ClientParam) (*HTTPClient, error) {
return nil, trace.NotImplemented("not implemented")
}

// NewAdminAuthServer returns auth server authorized as admin,
Expand Down
Loading