Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[v14] Allow headless auth when local auth is disabled #43363

Merged
merged 3 commits into from
Jun 24, 2024
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
24 changes: 0 additions & 24 deletions lib/auth/auth_login_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1454,20 +1454,6 @@ func TestServer_Authenticate_headless(t *testing.T) {
mfa := configureForMFA(t, srv)
username := mfa.User

// Fail a login attempt so we have a non-empty list of attempts.
_, err = proxyClient.AuthenticateSSHUser(ctx, authclient.AuthenticateSSHRequest{
AuthenticateUserRequest: authclient.AuthenticateUserRequest{
Username: username,
Webauthn: &wantypes.CredentialAssertionResponse{}, // bad response
PublicKey: []byte(sshPubKey),
},
TTL: 24 * time.Hour,
})
require.True(t, trace.IsAccessDenied(err), "got err = %v, want AccessDenied", err)
attempts, err := srv.Auth().GetUserLoginAttempts(username)
require.NoError(t, err)
require.NotEmpty(t, attempts, "Want at least one failed login attempt")

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

Expand Down Expand Up @@ -1518,18 +1504,8 @@ func TestServer_Authenticate_headless(t *testing.T) {

if tc.expectErr {
require.Error(t, err)
// Verify login attempts unchanged. This is a proxy for various other user
// checks (locked, etc).
updatedAttempts, err := srv.Auth().GetUserLoginAttempts(username)
require.NoError(t, err)
require.Equal(t, attempts, updatedAttempts, "Login attempts unexpectedly changed")
} else {
require.NoError(t, err)
// Verify zeroed login attempts. This is a proxy for various other user
// checks (locked, etc).
updatedAttempts, err := srv.Auth().GetUserLoginAttempts(username)
require.NoError(t, err)
require.Empty(t, updatedAttempts, "Login attempts not reset")
}
})
}
Expand Down
31 changes: 15 additions & 16 deletions lib/auth/methods.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,9 +189,6 @@ func (a *Server) emitAuthAuditEvent(ctx context.Context, props authAuditProps) e
}

var (
// authenticateHeadlessError is the generic error returned for failed headless
// authentication attempts.
authenticateHeadlessError = &trace.AccessDeniedError{Message: "headless authentication failed"}
// authenticateWebauthnError is the generic error returned for failed WebAuthn
// authentication attempts.
authenticateWebauthnError = &trace.AccessDeniedError{Message: "invalid Webauthn response"}
Expand Down Expand Up @@ -275,6 +272,18 @@ func (a *Server) authenticateUserInternal(ctx context.Context, req authclient.Au
user = req.Username
passwordless := user == ""

// Headless auth is treated more like sso login or access requests than local login,
// meaning it should be allowed for non-local users and failed headless auth attempts
// should not lock out the user (we skip the WithUserLock wrapper).
if req.HeadlessAuthenticationID != "" {
mfaDev, err = a.authenticateHeadless(ctx, req)
if err != nil {
log.Debugf("Headless Authentication for user %q failed while waiting for approval: %v", user, err)
return nil, "", trace.Wrap(err)
}
return mfaDev, user, nil
}

// Only one path if passwordless, other variants shouldn't see an empty user.
if passwordless {
return a.authenticatePasswordless(ctx, req)
Expand All @@ -301,18 +310,6 @@ func (a *Server) authenticateUserInternal(ctx context.Context, req authclient.Au
var authErr error // error message kept obscure on purpose, use logging for details
switch {
// cases in order of preference
case req.HeadlessAuthenticationID != "":
// handle authentication before the user lock to prevent locking out users
// due to timed-out/canceled headless authentication attempts.
mfaDevice, err := a.authenticateHeadless(ctx, req)
if err != nil {
log.Debugf("Headless Authentication for user %q failed while waiting for approval: %v", user, err)
return nil, "", trace.Wrap(authenticateHeadlessError)
}
authenticateFn = func() (*types.MFADevice, error) {
return mfaDevice, nil
}
authErr = authenticateHeadlessError
case req.Webauthn != nil:
authenticateFn = func() (*types.MFADevice, error) {
if req.Pass != nil {
Expand Down Expand Up @@ -609,7 +606,9 @@ func (a *Server) AuthenticateSSHUser(ctx context.Context, req authclient.Authent
if err != nil {
return nil, trace.Wrap(err)
}
if !authPref.GetAllowLocalAuth() {

// Disable all local auth requests, except headless requests.
if !authPref.GetAllowLocalAuth() && req.HeadlessAuthenticationID == "" {
a.emitNoLocalAuthEvent(username)
return nil, trace.AccessDenied(noLocalAuth)
}
Expand Down
Loading