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

Move Automatic Cloud Passwordless preference setting to the Auth service #35807

Merged
merged 7 commits into from
Dec 21, 2023
45 changes: 43 additions & 2 deletions lib/auth/auth_with_roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -3133,7 +3133,49 @@ func (a *ServerWithRoles) GetResetPasswordToken(ctx context.Context, tokenID str
// ChangeUserAuthentication is implemented by AuthService.ChangeUserAuthentication.
func (a *ServerWithRoles) ChangeUserAuthentication(ctx context.Context, req *proto.ChangeUserAuthenticationRequest) (*proto.ChangeUserAuthenticationResponse, error) {
// Token is it's own authentication, no need to double check.
return a.authServer.ChangeUserAuthentication(ctx, req)
resp, err := a.authServer.ChangeUserAuthentication(ctx, req)
if err != nil {
return nil, trace.Wrap(err)
}

// We use the presence of a WebAuthn response, along with the absence of a
// password, as a proxy to determine that a passwordless registration took
// place, as it is not possible to infer that just from the WebAuthn response.
isPasswordless := req.NewMFARegisterResponse != nil && len(req.NewPassword) == 0
if isPasswordless && modules.GetModules().Features().Cloud {
if err := a.trySettingConnectorNameToPasswordless(ctx); err != nil {
log.WithError(err).Error("Failed to set passwordless as connector name.")
}
}

return resp, nil
}

// trySettingConnectorNameToPasswordless sets cluster_auth_preference connectorName to `passwordless` when the first cloud user chooses passwordless as the authentication method.
// This simplifies UX for cloud users, as they will not need to select a passwordless connector when logging in.
func (a *ServerWithRoles) trySettingConnectorNameToPasswordless(ctx context.Context) error {
users, err := a.authServer.GetUsers(ctx, false)
if err != nil {
return trace.Wrap(err)
}

// Only set the connector name on the first user registration.
if len(users) != 1 {
return nil
}

authPreference, err := a.authServer.GetAuthPreference(ctx)
if err != nil {
return trace.Wrap(err)
}

// Don't overwrite an existing connector name.
if connector := authPreference.GetConnectorName(); connector != "" && connector != constants.LocalConnector {
return nil
}

authPreference.SetConnectorName(constants.PasswordlessConnector)
return trace.Wrap(a.authServer.SetAuthPreference(ctx, authPreference))
}

// UpdateUser updates an existing user in a backend.
Expand Down Expand Up @@ -6973,7 +7015,6 @@ func checkOktaLockTarget(ctx context.Context, authzCtx *authz.Context, users ser
// checkOktaLockAccess gates access to update operations on lock records based
// on the origin label on the supplied user record.
func checkOktaLockAccess(ctx context.Context, authzCtx *authz.Context, locks services.LockGetter, existingLockName string, verb string) error {

existingLock, err := locks.GetLock(ctx, existingLockName)
if err != nil && !trace.IsNotFound(err) {
return trace.Wrap(err)
Expand Down
44 changes: 0 additions & 44 deletions lib/web/apiserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -2266,11 +2266,6 @@ func (h *Handler) changeUserAuthentication(w http.ResponseWriter, r *http.Reques
return nil, trace.Wrap(err)
}

err = h.trySettingConnectorNameToPasswordless(r.Context(), ctx, req)
if err != nil {
h.log.WithError(err).Error("Failed to set passwordless as connector name.")
}

if err := websession.SetCookie(w, sess.GetUser(), sess.GetName()); err != nil {
return nil, trace.Wrap(err)
}
Expand All @@ -2292,45 +2287,6 @@ func (h *Handler) changeUserAuthentication(w http.ResponseWriter, r *http.Reques
}, nil
}

// trySettingConnectorNameToPasswordless sets cluster_auth_preference connectorName to `passwordless` when the first cloud user chooses passwordless as the authentication method.
// This simplifies UX for cloud users, as they will not need to select a passwordless connector when logging in.
func (h *Handler) trySettingConnectorNameToPasswordless(ctx context.Context, sessCtx *SessionContext, req changeUserAuthenticationRequest) error {
// We use the presence of a WebAuthn response, along with the absence of a
// password, as a proxy to determine that a passwordless registration took
// place, as it is not possible to infer that just from the WebAuthn response.
isPasswordlessRegistration := req.WebauthnCreationResponse != nil && len(req.Password) == 0
if !isPasswordlessRegistration {
return nil
}

if !h.ClusterFeatures.GetCloud() {
return nil
}

authPreference, err := sessCtx.cfg.RootClient.GetAuthPreference(ctx)
if err != nil {
return nil
}

if connector := authPreference.GetConnectorName(); connector != "" && connector != constants.LocalConnector {
return nil
}

users, err := h.cfg.ProxyClient.GetUsers(ctx, false)
if err != nil {
return trace.Wrap(err)
}

if len(users) != 1 {
return nil
}

authPreference.SetConnectorName(constants.PasswordlessConnector)

err = sessCtx.cfg.RootClient.SetAuthPreference(ctx, authPreference)
return trace.Wrap(err)
}

// createResetPasswordToken allows a UI user to reset a user's password.
// This handler is also required for after creating new users.
func (h *Handler) createResetPasswordToken(w http.ResponseWriter, r *http.Request, _ httprouter.Params, ctx *SessionContext) (interface{}, error) {
Expand Down
154 changes: 78 additions & 76 deletions lib/web/apiserver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5577,7 +5577,7 @@ func TestChangeUserAuthentication_settingDefaultClusterAuthPreference(t *testing
initialConnectorName string
resultConnectorName string
}{{
name: "first cloud sign-in changes connector to `passwordless`",
name: "first cloud sign-in changes connector to passwordless",
cloud: true,
numberOfUsers: 1,
authPreferenceType: constants.Local,
Expand Down Expand Up @@ -5615,107 +5615,109 @@ func TestChangeUserAuthentication_settingDefaultClusterAuthPreference(t *testing
}}

for _, tc := range tt {
modules.SetTestModules(t, &modules.TestModules{
TestFeatures: modules.Features{
Cloud: tc.cloud,
},
})
t.Run(tc.name, func(t *testing.T) {
modules.SetTestModules(t, &modules.TestModules{
TestFeatures: modules.Features{
Cloud: tc.cloud,
},
})

const RPID = "localhost"
const RPID = "localhost"

s := newWebSuiteWithConfig(t, webSuiteConfig{
authPreferenceSpec: &types.AuthPreferenceSpecV2{
Type: tc.authPreferenceType,
ConnectorName: tc.initialConnectorName,
SecondFactor: constants.SecondFactorOn,
Webauthn: &types.Webauthn{
RPID: RPID,
s := newWebSuiteWithConfig(t, webSuiteConfig{
authPreferenceSpec: &types.AuthPreferenceSpecV2{
Type: tc.authPreferenceType,
ConnectorName: tc.initialConnectorName,
SecondFactor: constants.SecondFactorOn,
Webauthn: &types.Webauthn{
RPID: RPID,
},
},
},
})
})

// user and role
users := make([]types.User, tc.numberOfUsers)
// user and role
users := make([]types.User, tc.numberOfUsers)

for i := 0; i < tc.numberOfUsers; i++ {
user, err := types.NewUser(fmt.Sprintf("test_user_%v", i))
require.NoError(t, err)
for i := 0; i < tc.numberOfUsers; i++ {
user, err := types.NewUser(fmt.Sprintf("test_user_%v", i))
require.NoError(t, err)

user.SetCreatedBy(types.CreatedBy{
User: types.UserRef{Name: "other_user"},
})
user.SetCreatedBy(types.CreatedBy{
User: types.UserRef{Name: "other_user"},
})

role := services.RoleForUser(user)
role := services.RoleForUser(user)

role, err = s.server.Auth().UpsertRole(s.ctx, role)
require.NoError(t, err)
role, err = s.server.Auth().UpsertRole(s.ctx, role)
require.NoError(t, err)

user.AddRole(role.GetName())
user.AddRole(role.GetName())

user, err = s.server.Auth().CreateUser(s.ctx, user)
require.NoError(t, err)
user, err = s.server.Auth().CreateUser(s.ctx, user)
require.NoError(t, err)

users[i] = user
}
users[i] = user
}

initialUser := users[0]
initialUser := users[0]

clt := s.client(t)
clt := s.client(t)

// create register challenge
token, err := s.server.Auth().CreateResetPasswordToken(s.ctx, auth.CreateUserTokenRequest{
Name: initialUser.GetName(),
})
require.NoError(t, err)
// create register challenge
token, err := s.server.Auth().CreateResetPasswordToken(s.ctx, auth.CreateUserTokenRequest{
Name: initialUser.GetName(),
})
require.NoError(t, err)

res, err := s.server.Auth().CreateRegisterChallenge(s.ctx, &authproto.CreateRegisterChallengeRequest{
TokenID: token.GetName(),
DeviceType: authproto.DeviceType_DEVICE_TYPE_WEBAUTHN,
DeviceUsage: authproto.DeviceUsage_DEVICE_USAGE_PASSWORDLESS,
})
require.NoError(t, err)
res, err := s.server.Auth().CreateRegisterChallenge(s.ctx, &authproto.CreateRegisterChallengeRequest{
TokenID: token.GetName(),
DeviceType: authproto.DeviceType_DEVICE_TYPE_WEBAUTHN,
DeviceUsage: authproto.DeviceUsage_DEVICE_USAGE_PASSWORDLESS,
})
require.NoError(t, err)

cc := wantypes.CredentialCreationFromProto(res.GetWebauthn())
cc := wantypes.CredentialCreationFromProto(res.GetWebauthn())

// use passwordless as auth method
device, err := mocku2f.Create()
require.NoError(t, err)
// use passwordless as auth method
device, err := mocku2f.Create()
require.NoError(t, err)

device.SetPasswordless()
device.SetPasswordless()

ccr, err := device.SignCredentialCreation("https://"+RPID, cc)
require.NoError(t, err)
ccr, err := device.SignCredentialCreation("https://"+RPID, cc)
require.NoError(t, err)

// send sign-in response to server
body, err := json.Marshal(changeUserAuthenticationRequest{
WebauthnCreationResponse: ccr,
TokenID: token.GetName(),
DeviceName: "passwordless-device",
Password: tc.password,
})
require.NoError(t, err)
// send sign-in response to server
body, err := json.Marshal(changeUserAuthenticationRequest{
WebauthnCreationResponse: ccr,
TokenID: token.GetName(),
DeviceName: "passwordless-device",
Password: tc.password,
})
require.NoError(t, err)

req, err := http.NewRequest("PUT", clt.Endpoint("webapi", "users", "password", "token"), bytes.NewBuffer(body))
require.NoError(t, err)
req, err := http.NewRequest("PUT", clt.Endpoint("webapi", "users", "password", "token"), bytes.NewBuffer(body))
require.NoError(t, err)

csrfToken, err := csrf.GenerateToken()
require.NoError(t, err)
addCSRFCookieToReq(req, csrfToken)
req.Header.Set(csrf.HeaderName, csrfToken)
req.Header.Set("Content-Type", "application/json")
csrfToken, err := csrf.GenerateToken()
require.NoError(t, err)
addCSRFCookieToReq(req, csrfToken)
req.Header.Set(csrf.HeaderName, csrfToken)
req.Header.Set("Content-Type", "application/json")

re, err := clt.Client.RoundTrip(func() (*http.Response, error) {
return clt.Client.HTTPClient().Do(req)
})
re, err := clt.Client.RoundTrip(func() (*http.Response, error) {
return clt.Client.HTTPClient().Do(req)
})

require.NoError(t, err)
require.Equal(t, http.StatusOK, re.Code())
require.NoError(t, err)
require.Equal(t, http.StatusOK, re.Code())

// check if auth preference connectorName is set
authPreference, err := s.server.Auth().GetAuthPreference(s.ctx)
require.NoError(t, err)
// check if auth preference connectorName is set
authPreference, err := s.server.Auth().GetAuthPreference(s.ctx)
require.NoError(t, err)

require.Equal(t, authPreference.GetConnectorName(), tc.resultConnectorName, "Found unexpected auth connector name")
require.Equal(t, tc.resultConnectorName, authPreference.GetConnectorName(), "Found unexpected auth connector name")
})
}
}

Expand Down
Loading