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
23 changes: 12 additions & 11 deletions lib/auth/auth_login_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,8 @@ func TestServer_AuthenticateUser_mfaDevices(t *testing.T) {
// Solve challenge (client-side)
resp, err := test.solveChallenge(challenge)
authReq := AuthenticateUserRequest{
Username: username,
Username: username,
PublicKey: []byte(sshPubKey),
}
require.NoError(t, err)

Expand All @@ -463,7 +464,6 @@ func TestServer_AuthenticateUser_mfaDevices(t *testing.T) {
t.Run(test.name+"/ssh", makeRun(func(s *Server, req AuthenticateUserRequest) error {
_, err := s.AuthenticateSSHUser(ctx, AuthenticateSSHRequest{
AuthenticateUserRequest: req,
PublicKey: []byte(sshPubKey),
TTL: 24 * time.Hour,
})
return err
Expand Down Expand Up @@ -560,10 +560,10 @@ func TestServer_Authenticate_passwordless(t *testing.T) {
authenticate: func(t *testing.T, resp *wanlib.CredentialAssertionResponse) {
loginResp, err := proxyClient.AuthenticateSSHUser(ctx, AuthenticateSSHRequest{
AuthenticateUserRequest: AuthenticateUserRequest{
Webauthn: resp,
Webauthn: resp,
PublicKey: []byte(sshPubKey),
},
PublicKey: []byte(sshPubKey),
TTL: 24 * time.Hour,
TTL: 24 * time.Hour,
})
require.NoError(t, err, "Failed to perform passwordless authentication")
require.NotNil(t, loginResp, "SSH response nil")
Expand All @@ -587,11 +587,11 @@ func TestServer_Authenticate_passwordless(t *testing.T) {
// Fail a login attempt so have a non-empty list of attempts.
_, err := proxyClient.AuthenticateSSHUser(ctx, AuthenticateSSHRequest{
AuthenticateUserRequest: AuthenticateUserRequest{
Username: user,
Webauthn: &wanlib.CredentialAssertionResponse{}, // bad response
Username: user,
Webauthn: &wanlib.CredentialAssertionResponse{}, // bad response
PublicKey: []byte(sshPubKey),
},
PublicKey: []byte(sshPubKey),
TTL: 24 * time.Hour,
TTL: 24 * time.Hour,
})
require.True(t, trace.IsAccessDenied(err), "got err = %v, want AccessDenied")
attempts, err := authServer.GetUserLoginAttempts(user)
Expand Down Expand Up @@ -667,7 +667,9 @@ func TestServer_Authenticate_nonPasswordlessRequiresUsername(t *testing.T) {
mfaResp, err := test.dev.SolveAuthn(mfaChallenge)
require.NoError(t, err)

var req AuthenticateUserRequest
req := AuthenticateUserRequest{
PublicKey: []byte(sshPubKey),
}
switch {
case mfaResp.GetWebauthn() != nil:
req.Webauthn = wanlib.CredentialAssertionResponseFromProto(mfaResp.GetWebauthn())
Expand All @@ -681,7 +683,6 @@ func TestServer_Authenticate_nonPasswordlessRequiresUsername(t *testing.T) {
// SSH.
_, err = proxyClient.AuthenticateSSHUser(ctx, AuthenticateSSHRequest{
AuthenticateUserRequest: req,
PublicKey: []byte(sshPubKey),
TTL: 24 * time.Hour,
})
require.Error(t, err, "SSH authentication expected fail (missing username)")
Expand Down
54 changes: 27 additions & 27 deletions lib/auth/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,10 +286,10 @@ func TestAuthenticateSSHUser(t *testing.T) {
// Login to the root cluster.
resp, err := s.a.AuthenticateSSHUser(ctx, AuthenticateSSHRequest{
AuthenticateUserRequest: AuthenticateUserRequest{
Username: user,
Pass: &PassCreds{Password: pass},
Username: user,
Pass: &PassCreds{Password: pass},
PublicKey: pub,
},
PublicKey: pub,
TTL: time.Hour,
RouteToCluster: s.clusterName.GetClusterName(),
})
Expand Down Expand Up @@ -325,10 +325,10 @@ func TestAuthenticateSSHUser(t *testing.T) {
// Login to the leaf cluster.
resp, err = s.a.AuthenticateSSHUser(ctx, AuthenticateSSHRequest{
AuthenticateUserRequest: AuthenticateUserRequest{
Username: user,
Pass: &PassCreds{Password: pass},
Username: user,
Pass: &PassCreds{Password: pass},
PublicKey: pub,
},
PublicKey: pub,
TTL: time.Hour,
RouteToCluster: "leaf.localhost",
KubernetesCluster: "leaf-kube-cluster",
Expand Down Expand Up @@ -373,10 +373,10 @@ func TestAuthenticateSSHUser(t *testing.T) {
// Login specifying a valid kube cluster. It should appear in the TLS cert.
resp, err = s.a.AuthenticateSSHUser(ctx, AuthenticateSSHRequest{
AuthenticateUserRequest: AuthenticateUserRequest{
Username: user,
Pass: &PassCreds{Password: pass},
Username: user,
Pass: &PassCreds{Password: pass},
PublicKey: pub,
},
PublicKey: pub,
TTL: time.Hour,
RouteToCluster: s.clusterName.GetClusterName(),
KubernetesCluster: "root-kube-cluster",
Expand Down Expand Up @@ -405,10 +405,10 @@ func TestAuthenticateSSHUser(t *testing.T) {
// automatically.
resp, err = s.a.AuthenticateSSHUser(ctx, AuthenticateSSHRequest{
AuthenticateUserRequest: AuthenticateUserRequest{
Username: user,
Pass: &PassCreds{Password: pass},
Username: user,
Pass: &PassCreds{Password: pass},
PublicKey: pub,
},
PublicKey: pub,
TTL: time.Hour,
RouteToCluster: s.clusterName.GetClusterName(),
// Intentionally empty, auth server should default to a registered
Expand Down Expand Up @@ -450,10 +450,10 @@ func TestAuthenticateSSHUser(t *testing.T) {
// Login specifying a valid kube cluster. It should appear in the TLS cert.
resp, err = s.a.AuthenticateSSHUser(ctx, AuthenticateSSHRequest{
AuthenticateUserRequest: AuthenticateUserRequest{
Username: user,
Pass: &PassCreds{Password: pass},
Username: user,
Pass: &PassCreds{Password: pass},
PublicKey: pub,
},
PublicKey: pub,
TTL: time.Hour,
RouteToCluster: s.clusterName.GetClusterName(),
KubernetesCluster: "root-kube-cluster",
Expand Down Expand Up @@ -482,10 +482,10 @@ func TestAuthenticateSSHUser(t *testing.T) {
// automatically.
resp, err = s.a.AuthenticateSSHUser(ctx, AuthenticateSSHRequest{
AuthenticateUserRequest: AuthenticateUserRequest{
Username: user,
Pass: &PassCreds{Password: pass},
Username: user,
Pass: &PassCreds{Password: pass},
PublicKey: pub,
},
PublicKey: pub,
TTL: time.Hour,
RouteToCluster: s.clusterName.GetClusterName(),
// Intentionally empty, auth server should default to a registered
Expand Down Expand Up @@ -515,10 +515,10 @@ func TestAuthenticateSSHUser(t *testing.T) {
// Login specifying an invalid kube cluster. This should fail.
_, err = s.a.AuthenticateSSHUser(ctx, AuthenticateSSHRequest{
AuthenticateUserRequest: AuthenticateUserRequest{
Username: user,
Pass: &PassCreds{Password: pass},
Username: user,
Pass: &PassCreds{Password: pass},
PublicKey: pub,
},
PublicKey: pub,
TTL: time.Hour,
RouteToCluster: s.clusterName.GetClusterName(),
KubernetesCluster: "invalid-kube-cluster",
Expand Down Expand Up @@ -1155,9 +1155,9 @@ func TestServer_AugmentContextUserCertificates(t *testing.T) {
Pass: &PassCreds{
Password: []byte(pass),
},
PublicKey: pub,
},
PublicKey: pub,
TTL: 1 * time.Hour,
TTL: 1 * time.Hour,
})
require.NoError(t, err, "AuthenticateSSHUser failed")

Expand Down Expand Up @@ -1312,9 +1312,9 @@ func TestServer_AugmentContextUserCertificates_errors(t *testing.T) {
Pass: &PassCreds{
Password: []byte(pass),
},
PublicKey: ssh.MarshalAuthorizedKey(sPubKey),
},
PublicKey: ssh.MarshalAuthorizedKey(sPubKey),
TTL: 1 * time.Hour,
TTL: 1 * time.Hour,
})
require.NoError(t, err, "AuthenticateSSHUser(%q) failed", user)

Expand Down Expand Up @@ -1779,10 +1779,10 @@ func TestGenerateUserCertIPPinning(t *testing.T) {

baseAuthRequest := AuthenticateSSHRequest{
AuthenticateUserRequest: AuthenticateUserRequest{
Pass: &PassCreds{Password: pass},
Pass: &PassCreds{Password: pass},
PublicKey: pub,
},
TTL: time.Hour,
PublicKey: pub,
RouteToCluster: s.clusterName.GetClusterName(),
}

Expand Down
14 changes: 8 additions & 6 deletions lib/auth/methods.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ const (
type AuthenticateUserRequest struct {
// Username is a username
Username string `json:"username"`
// PublicKey is a public key in ssh authorized_keys format
PublicKey []byte `json:"public_key"`
// Pass is a password used in local authentication schemes
Pass *PassCreds `json:"pass,omitempty"`
// Webauthn is a signed credential assertion, used in MFA authentication
Expand All @@ -60,6 +62,8 @@ type AuthenticateUserRequest struct {
Session *SessionCreds `json:"session,omitempty"`
// ClientMetadata includes forwarded information about a client
ClientMetadata *ForwardedClientMetadata `json:"client_metadata,omitempty"`
// HeadlessAuthenticationID is the ID for a headless authentication resource.
HeadlessAuthenticationID string `json:"headless_authentication_id"`
}

// ForwardedClientMetadata can be used by the proxy web API to forward information about
Expand Down Expand Up @@ -103,10 +107,10 @@ type SessionCreds struct {

// AuthenticateUser authenticates user based on the request type.
// Returns the username of the authenticated user.
func (s *Server) AuthenticateUser(req AuthenticateUserRequest) (string, error) {
func (s *Server) AuthenticateUser(ctx context.Context, req AuthenticateUserRequest) (string, error) {
user := req.Username

mfaDev, actualUser, err := s.authenticateUser(context.TODO(), req)
mfaDev, actualUser, err := s.authenticateUser(ctx, req)
// err is handled below.
switch {
case user != "" && actualUser != "" && user != actualUser:
Expand Down Expand Up @@ -339,7 +343,7 @@ func (s *Server) AuthenticateWebUser(ctx context.Context, req AuthenticateUserRe
return session, nil
}

actualUser, err := s.AuthenticateUser(req)
actualUser, err := s.AuthenticateUser(ctx, req)
if err != nil {
return nil, trace.Wrap(err)
}
Expand All @@ -362,8 +366,6 @@ func (s *Server) AuthenticateWebUser(ctx context.Context, req AuthenticateUserRe
type AuthenticateSSHRequest struct {
// AuthenticateUserRequest is a request with credentials
AuthenticateUserRequest
// PublicKey is a public key in ssh authorized_keys format
PublicKey []byte `json:"public_key"`
// TTL is a requested TTL for certificates to be issues
TTL time.Duration `json:"ttl"`
// CompatibilityMode sets certificate compatibility mode with old SSH clients
Expand Down Expand Up @@ -465,7 +467,7 @@ func (s *Server) AuthenticateSSHUser(ctx context.Context, req AuthenticateSSHReq
return nil, trace.Wrap(err)
}

actualUser, err := s.AuthenticateUser(req.AuthenticateUserRequest)
actualUser, err := s.AuthenticateUser(ctx, req.AuthenticateUserRequest)
if err != nil {
return nil, trace.Wrap(err)
}
Expand Down
4 changes: 3 additions & 1 deletion lib/auth/methods_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
package auth

import (
"context"
"strings"
"testing"

Expand All @@ -27,14 +28,15 @@ import (
)

func TestServerAuthenticateUserUserAgentTrim(t *testing.T) {
ctx := context.Background()
emitter := &eventstest.MockEmitter{}
r := AuthenticateUserRequest{
ClientMetadata: &ForwardedClientMetadata{
UserAgent: strings.Repeat("A", maxUserAgentLen+1),
},
}
// Ignoring the error here because we really just care that the event was logged.
(&Server{emitter: emitter}).AuthenticateUser(r)
(&Server{emitter: emitter}).AuthenticateUser(ctx, r)
event := emitter.LastEvent()
loginEvent, ok := event.(*apievents.UserLogin)
require.True(t, ok)
Expand Down
4 changes: 2 additions & 2 deletions lib/auth/tls_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2399,10 +2399,10 @@ func TestCertificateFormat(t *testing.T) {
Pass: &PassCreds{
Password: pass,
},
PublicKey: pub,
},
CompatibilityMode: ts.inClientCertificateFormat,
TTL: apidefaults.CertDuration,
PublicKey: pub,
})
require.NoError(t, err)

Expand Down Expand Up @@ -2685,8 +2685,8 @@ func TestLoginNoLocalAuth(t *testing.T) {
Pass: &PassCreds{
Password: pass,
},
PublicKey: pub,
},
PublicKey: pub,
})
require.True(t, trace.IsAccessDenied(err))
}
Expand Down
45 changes: 45 additions & 0 deletions lib/client/weblogin.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ type CreateSSHCertReq struct {
Password string `json:"password"`
// OTPToken is second factor token
OTPToken string `json:"otp_token"`
// HeadlessAuthenticationID is a headless authentication resource id.
HeadlessAuthenticationID string `json:"headless_id"`
// PubKey is a public key user wishes to sign
PubKey []byte `json:"pub_key"`
// TTL is a desired TTL for the cert (max is still capped by server,
Expand Down Expand Up @@ -281,6 +283,16 @@ type SSHLoginPasswordless struct {
CustomPrompt wancli.LoginPrompt
}

type SSHLoginHeadless struct {
SSHLogin

// User is the login username.
User string

// HeadlessAuthenticationID is a headless authentication request ID.
HeadlessAuthenticationID string
}

// initClient creates a new client to the HTTPS web proxy.
func initClient(proxyAddr string, insecure bool, pool *x509.CertPool, extraHeaders map[string]string) (*WebClient, *url.URL, error) {
log := logrus.WithFields(logrus.Fields{
Expand Down Expand Up @@ -416,6 +428,39 @@ func SSHAgentLogin(ctx context.Context, login SSHLoginDirect) (*auth.SSHLoginRes
return out, nil
}

// SSHAgentHeadlessLogin begins the headless login ceremony, returning new user certificates if successful.
func SSHAgentHeadlessLogin(ctx context.Context, login SSHLoginHeadless) (*auth.SSHLoginResponse, error) {
clt, _, err := initClient(login.ProxyAddr, login.Insecure, login.Pool, login.ExtraHeaders)
if err != nil {
return nil, trace.Wrap(err)
}

// This request will block until the headless login is approved.
clt.Client.HTTPClient().Timeout = defaults.CallbackTimeout

re, err := clt.PostJSON(ctx, clt.Endpoint("webapi", "ssh", "certs"), CreateSSHCertReq{
User: login.User,
HeadlessAuthenticationID: login.HeadlessAuthenticationID,
PubKey: login.PubKey,
TTL: login.TTL,
Compatibility: login.Compatibility,
RouteToCluster: login.RouteToCluster,
KubernetesCluster: login.KubernetesCluster,
AttestationStatement: login.AttestationStatement,
})
if err != nil {
return nil, trace.Wrap(err)
}

var out *auth.SSHLoginResponse
err = json.Unmarshal(re.Bytes(), &out)
if err != nil {
return nil, trace.Wrap(err)
}

return out, nil
}

// SSHAgentPasswordlessLogin requests a passwordless MFA challenge via the proxy.
// weblogin.CustomPrompt (or a default prompt) is used for interaction with the
// end user.
Expand Down
Loading