From 30a10d35a8406f4af96fcc8200c4e2173856837d Mon Sep 17 00:00:00 2001 From: Joram Wilander Date: Mon, 12 Dec 2016 08:16:10 -0500 Subject: PLT-4767 Implement MFA Enforcement (#4662) * Create MFA setup page and remove MFA setup from account settings modal * Add enforce MFA to system console and force redirect * Lockdown mfa required API routes, add localization, other changes * Minor fixes * Fix typo * Fix some unit tests * Fix more unit tests * Minor fix * Updating UI for MFA screen (#4670) * Updating UI for MFA screen * Updating styles for MFA page * Add the ability to switch between email/sso with MFA enabled * Added mfa change email * Minor UI updates for MFA enforcement * Fix unit test * Fix client unit test * Allow switching email to ldap and back when MFA is enabled * Fix unit test * Revert config.json --- api/channel_test.go | 20 ++++++++++++++++ api/context.go | 66 +++++++++++++++++++++++++++++++++++++++++++---------- api/oauth.go | 2 +- api/team_test.go | 4 ++++ api/user.go | 59 +++++++++++++++++++++++++++++++++++++++++++---- api/user_test.go | 2 +- 6 files changed, 135 insertions(+), 18 deletions(-) (limited to 'api') diff --git a/api/channel_test.go b/api/channel_test.go index 2414b51e2..11914414b 100644 --- a/api/channel_test.go +++ b/api/channel_test.go @@ -95,18 +95,22 @@ func TestCreateChannel(t *testing.T) { } isLicensed := utils.IsLicensed + license := utils.License restrictPublicChannel := *utils.Cfg.TeamSettings.RestrictPublicChannelManagement restrictPrivateChannel := *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement defer func() { *utils.Cfg.TeamSettings.RestrictPublicChannelManagement = restrictPublicChannel *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = restrictPrivateChannel utils.IsLicensed = isLicensed + utils.License = license utils.SetDefaultRolesBasedOnConfig() }() *utils.Cfg.TeamSettings.RestrictPublicChannelManagement = model.PERMISSIONS_ALL *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = model.PERMISSIONS_ALL utils.SetDefaultRolesBasedOnConfig() utils.IsLicensed = true + utils.License = &model.License{Features: &model.Features{}} + utils.License.Features.SetDefaults() channel2 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} channel3 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_PRIVATE, TeamId: team.Id} @@ -288,17 +292,21 @@ func TestUpdateChannel(t *testing.T) { } isLicensed := utils.IsLicensed + license := utils.License restrictPublicChannel := *utils.Cfg.TeamSettings.RestrictPublicChannelManagement restrictPrivateChannel := *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement defer func() { *utils.Cfg.TeamSettings.RestrictPublicChannelManagement = restrictPublicChannel *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = restrictPrivateChannel utils.IsLicensed = isLicensed + utils.License = license utils.SetDefaultRolesBasedOnConfig() }() *utils.Cfg.TeamSettings.RestrictPublicChannelManagement = model.PERMISSIONS_ALL *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = model.PERMISSIONS_ALL utils.IsLicensed = true + utils.License = &model.License{Features: &model.Features{}} + utils.License.Features.SetDefaults() utils.SetDefaultRolesBasedOnConfig() channel2 := th.CreateChannel(Client, team) @@ -436,17 +444,21 @@ func TestUpdateChannelHeader(t *testing.T) { } isLicensed := utils.IsLicensed + license := utils.License restrictPublicChannel := *utils.Cfg.TeamSettings.RestrictPublicChannelManagement restrictPrivateChannel := *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement defer func() { *utils.Cfg.TeamSettings.RestrictPublicChannelManagement = restrictPublicChannel *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = restrictPrivateChannel utils.IsLicensed = isLicensed + utils.License = license utils.SetDefaultRolesBasedOnConfig() }() *utils.Cfg.TeamSettings.RestrictPublicChannelManagement = model.PERMISSIONS_ALL *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = model.PERMISSIONS_ALL utils.IsLicensed = true + utils.License = &model.License{Features: &model.Features{}} + utils.License.Features.SetDefaults() utils.SetDefaultRolesBasedOnConfig() th.LoginBasic() @@ -566,17 +578,21 @@ func TestUpdateChannelPurpose(t *testing.T) { } isLicensed := utils.IsLicensed + license := utils.License restrictPublicChannel := *utils.Cfg.TeamSettings.RestrictPublicChannelManagement restrictPrivateChannel := *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement defer func() { *utils.Cfg.TeamSettings.RestrictPublicChannelManagement = restrictPublicChannel *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = restrictPrivateChannel utils.IsLicensed = isLicensed + utils.License = license utils.SetDefaultRolesBasedOnConfig() }() *utils.Cfg.TeamSettings.RestrictPublicChannelManagement = model.PERMISSIONS_ALL *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = model.PERMISSIONS_ALL utils.IsLicensed = true + utils.License = &model.License{Features: &model.Features{}} + utils.License.Features.SetDefaults() utils.SetDefaultRolesBasedOnConfig() th.LoginBasic() @@ -1071,17 +1087,21 @@ func TestDeleteChannel(t *testing.T) { } isLicensed := utils.IsLicensed + license := utils.License restrictPublicChannel := *utils.Cfg.TeamSettings.RestrictPublicChannelManagement restrictPrivateChannel := *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement defer func() { *utils.Cfg.TeamSettings.RestrictPublicChannelManagement = restrictPublicChannel *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = restrictPrivateChannel utils.IsLicensed = isLicensed + utils.License = license utils.SetDefaultRolesBasedOnConfig() }() *utils.Cfg.TeamSettings.RestrictPublicChannelManagement = model.PERMISSIONS_ALL *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = model.PERMISSIONS_ALL utils.IsLicensed = true + utils.License = &model.License{Features: &model.Features{}} + utils.License.Features.SetDefaults() utils.SetDefaultRolesBasedOnConfig() th.LoginSystemAdmin() diff --git a/api/context.go b/api/context.go index 4c2e9d489..1e82acb68 100644 --- a/api/context.go +++ b/api/context.go @@ -47,51 +47,55 @@ type Context struct { } func ApiAppHandler(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { - return &handler{h, false, false, true, false, false, false} + return &handler{h, false, false, true, false, false, false, false} } func AppHandler(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { - return &handler{h, false, false, false, false, false, false} + return &handler{h, false, false, false, false, false, false, false} } func AppHandlerIndependent(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { - return &handler{h, false, false, false, false, true, false} + return &handler{h, false, false, false, false, true, false, false} } func ApiUserRequired(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { - return &handler{h, true, false, true, false, false, false} + return &handler{h, true, false, true, false, false, false, true} } func ApiUserRequiredActivity(h func(*Context, http.ResponseWriter, *http.Request), isUserActivity bool) http.Handler { - return &handler{h, true, false, true, isUserActivity, false, false} + return &handler{h, true, false, true, isUserActivity, false, false, true} +} + +func ApiUserRequiredMfa(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { + return &handler{h, true, false, true, false, false, false, false} } func UserRequired(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { - return &handler{h, true, false, false, false, false, false} + return &handler{h, true, false, false, false, false, false, true} } func AppHandlerTrustRequester(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { - return &handler{h, false, false, false, false, false, true} + return &handler{h, false, false, false, false, false, true, false} } func ApiAdminSystemRequired(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { - return &handler{h, true, true, true, false, false, false} + return &handler{h, true, true, true, false, false, false, true} } func ApiAdminSystemRequiredTrustRequester(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { - return &handler{h, true, true, true, false, false, true} + return &handler{h, true, true, true, false, false, true, true} } func ApiAppHandlerTrustRequester(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { - return &handler{h, false, false, true, false, false, true} + return &handler{h, false, false, true, false, false, true, false} } func ApiUserRequiredTrustRequester(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { - return &handler{h, true, false, true, false, false, true} + return &handler{h, true, false, true, false, false, true, true} } func ApiAppHandlerTrustRequesterIndependent(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { - return &handler{h, false, false, true, false, true, true} + return &handler{h, false, false, true, false, true, true, false} } type handler struct { @@ -102,6 +106,7 @@ type handler struct { isUserActivity bool isTeamIndependent bool trustRequester bool + requireMfa bool } func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { @@ -204,6 +209,10 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { c.UserRequired() } + if c.Err == nil && h.requireMfa { + c.MfaRequired() + } + if c.Err == nil && h.requireSystemAdmin { c.SystemAdminRequired() } @@ -331,6 +340,39 @@ func (c *Context) UserRequired() { } } +func (c *Context) MfaRequired() { + // Must be licensed for MFA and have it configured for enforcement + if !utils.IsLicensed || !*utils.License.Features.MFA || !*utils.Cfg.ServiceSettings.EnableMultifactorAuthentication || !*utils.Cfg.ServiceSettings.EnforceMultifactorAuthentication { + return + } + + // OAuth integrations are excepted + if c.Session.IsOAuth { + return + } + + if result := <-Srv.Store.User().Get(c.Session.UserId); result.Err != nil { + c.Err = model.NewLocAppError("", "api.context.session_expired.app_error", nil, "MfaRequired") + c.Err.StatusCode = http.StatusUnauthorized + return + } else { + user := result.Data.(*model.User) + + // Only required for email and ldap accounts + if user.AuthService != "" && + user.AuthService != model.USER_AUTH_SERVICE_EMAIL && + user.AuthService != model.USER_AUTH_SERVICE_LDAP { + return + } + + if !user.MfaActive { + c.Err = model.NewLocAppError("", "api.context.mfa_required.app_error", nil, "MfaRequired") + c.Err.StatusCode = http.StatusUnauthorized + return + } + } +} + func (c *Context) SystemAdminRequired() { if len(c.Session.UserId) == 0 { c.Err = model.NewLocAppError("", "api.context.session_expired.app_error", nil, "SystemAdminRequired") diff --git a/api/oauth.go b/api/oauth.go index 08c763b61..268cf1aed 100644 --- a/api/oauth.go +++ b/api/oauth.go @@ -856,7 +856,7 @@ func CompleteSwitchWithOAuth(c *Context, w http.ResponseWriter, r *http.Request, return } - if result := <-Srv.Store.User().UpdateAuthData(user.Id, service, &authData, ssoEmail); result.Err != nil { + if result := <-Srv.Store.User().UpdateAuthData(user.Id, service, &authData, ssoEmail, true); result.Err != nil { c.Err = result.Err return } diff --git a/api/team_test.go b/api/team_test.go index 5880ffcff..2af46f4dc 100644 --- a/api/team_test.go +++ b/api/team_test.go @@ -426,10 +426,14 @@ func TestInviteMembers(t *testing.T) { } isLicensed := utils.IsLicensed + license := utils.License defer func() { utils.IsLicensed = isLicensed + utils.License = license }() utils.IsLicensed = true + utils.License = &model.License{Features: &model.Features{}} + utils.License.Features.SetDefaults() if _, err := Client.InviteMembers(invites); err == nil { t.Fatal("should have errored not team admin and licensed") diff --git a/api/user.go b/api/user.go index 2085ccb60..0e74a577c 100644 --- a/api/user.go +++ b/api/user.go @@ -65,8 +65,8 @@ func InitUser() { BaseRoutes.NeedChannel.Handle("/users/autocomplete", ApiUserRequired(autocompleteUsersInChannel)).Methods("GET") BaseRoutes.Users.Handle("/mfa", ApiAppHandler(checkMfa)).Methods("POST") - BaseRoutes.Users.Handle("/generate_mfa_secret", ApiUserRequiredTrustRequester(generateMfaSecret)).Methods("GET") - BaseRoutes.Users.Handle("/update_mfa", ApiUserRequired(updateMfa)).Methods("POST") + BaseRoutes.Users.Handle("/generate_mfa_secret", ApiUserRequiredMfa(generateMfaSecret)).Methods("GET") + BaseRoutes.Users.Handle("/update_mfa", ApiUserRequiredMfa(updateMfa)).Methods("POST") BaseRoutes.Users.Handle("/claim/email_to_oauth", ApiAppHandler(emailToOAuth)).Methods("POST") BaseRoutes.Users.Handle("/claim/oauth_to_email", ApiUserRequired(oauthToEmail)).Methods("POST") @@ -1875,6 +1875,30 @@ func sendPasswordChangeEmail(c *Context, email, siteURL, method string) { } } +func sendMfaChangeEmail(c *Context, email string, siteURL string, activated bool) { + subject := c.T("api.templates.mfa_change_subject", + map[string]interface{}{"SiteName": utils.Cfg.TeamSettings.SiteName}) + + bodyPage := utils.NewHTMLTemplate("mfa_change_body", c.Locale) + bodyPage.Props["SiteURL"] = siteURL + + bodyText := "" + if activated { + bodyText = "api.templates.mfa_activated_body.info" + bodyPage.Props["Title"] = c.T("api.templates.mfa_activated_body.title") + } else { + bodyText = "api.templates.mfa_deactivated_body.info" + bodyPage.Props["Title"] = c.T("api.templates.mfa_deactivated_body.title") + } + + bodyPage.Html["Info"] = template.HTML(c.T(bodyText, + map[string]interface{}{"SiteURL": siteURL})) + + if err := utils.SendMail(email, subject, bodyPage.Render()); err != nil { + l4g.Error(utils.T("api.user.send_mfa_change_email.error"), err) + } +} + func sendEmailChangeEmail(c *Context, oldEmail, newEmail, siteURL string) { subject := fmt.Sprintf("[%v] %v", utils.Cfg.TeamSettings.SiteName, c.T("api.templates.email_change_subject", map[string]interface{}{"TeamDisplayName": utils.Cfg.TeamSettings.SiteName})) @@ -2016,6 +2040,8 @@ func emailToOAuth(c *Context, w http.ResponseWriter, r *http.Request) { return } + mfaToken := props["token"] + service := props["service"] if len(service) == 0 { c.SetInvalidParam("emailToOAuth", "service") @@ -2039,7 +2065,7 @@ func emailToOAuth(c *Context, w http.ResponseWriter, r *http.Request) { user = result.Data.(*model.User) } - if err := checkPasswordAndAllCriteria(user, password, ""); err != nil { + if err := checkPasswordAndAllCriteria(user, password, mfaToken); err != nil { c.LogAuditWithUserId(user.Id, "failed - bad authentication") c.Err = err return @@ -2147,6 +2173,8 @@ func emailToLdap(c *Context, w http.ResponseWriter, r *http.Request) { return } + token := props["token"] + c.LogAudit("attempt") var user *model.User @@ -2158,7 +2186,7 @@ func emailToLdap(c *Context, w http.ResponseWriter, r *http.Request) { user = result.Data.(*model.User) } - if err := checkPasswordAndAllCriteria(user, emailPassword, ""); err != nil { + if err := checkPasswordAndAllCriteria(user, emailPassword, token); err != nil { c.LogAuditWithUserId(user.Id, "failed - bad authentication") c.Err = err return @@ -2213,6 +2241,8 @@ func ldapToEmail(c *Context, w http.ResponseWriter, r *http.Request) { return } + token := props["token"] + c.LogAudit("attempt") var user *model.User @@ -2242,6 +2272,12 @@ func ldapToEmail(c *Context, w http.ResponseWriter, r *http.Request) { return } + if err := checkUserMfa(user, token); err != nil { + c.LogAuditWithUserId(user.Id, "fail - mfa token failed") + c.Err = err + return + } + if result := <-Srv.Store.User().UpdatePassword(user.Id, model.HashPassword(emailPassword)); result.Err != nil { c.LogAudit("fail - database issue") c.Err = result.Err @@ -2379,18 +2415,33 @@ func updateMfa(c *Context, w http.ResponseWriter, r *http.Request) { } } + c.LogAudit("attempt") + if activate { if err := ActivateMfa(c.Session.UserId, token); err != nil { c.Err = err return } + c.LogAudit("success - activated") } else { if err := DeactivateMfa(c.Session.UserId); err != nil { c.Err = err return } + c.LogAudit("success - deactivated") } + go func() { + var user *model.User + if result := <-Srv.Store.User().Get(c.Session.UserId); result.Err != nil { + l4g.Warn(result.Err) + } else { + user = result.Data.(*model.User) + } + + sendMfaChangeEmail(c, user.Email, c.GetSiteURL(), activate) + }() + rdata := map[string]string{} rdata["status"] = "ok" w.Write([]byte(model.MapToJson(rdata))) diff --git a/api/user_test.go b/api/user_test.go index 85af7e598..65bdcb653 100644 --- a/api/user_test.go +++ b/api/user_test.go @@ -1341,7 +1341,7 @@ func TestResetPassword(t *testing.T) { } authData := model.NewId() - if result := <-Srv.Store.User().UpdateAuthData(user.Id, "random", &authData, ""); result.Err != nil { + if result := <-Srv.Store.User().UpdateAuthData(user.Id, "random", &authData, "", true); result.Err != nil { t.Fatal(result.Err) } -- cgit v1.2.3-1-g7c22