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
Этот коммит содержится в:
Joram Wilander
2016-12-12 08:16:10 -05:00
коммит произвёл enahum
родитель f0d71d8789
Коммит 30a10d35a8
34 изменённых файлов: 1001 добавлений и 231 удалений

Просмотреть файл

@@ -95,18 +95,22 @@ func TestCreateChannel(t *testing.T) {
} }
isLicensed := utils.IsLicensed isLicensed := utils.IsLicensed
license := utils.License
restrictPublicChannel := *utils.Cfg.TeamSettings.RestrictPublicChannelManagement restrictPublicChannel := *utils.Cfg.TeamSettings.RestrictPublicChannelManagement
restrictPrivateChannel := *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement restrictPrivateChannel := *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement
defer func() { defer func() {
*utils.Cfg.TeamSettings.RestrictPublicChannelManagement = restrictPublicChannel *utils.Cfg.TeamSettings.RestrictPublicChannelManagement = restrictPublicChannel
*utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = restrictPrivateChannel *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = restrictPrivateChannel
utils.IsLicensed = isLicensed utils.IsLicensed = isLicensed
utils.License = license
utils.SetDefaultRolesBasedOnConfig() utils.SetDefaultRolesBasedOnConfig()
}() }()
*utils.Cfg.TeamSettings.RestrictPublicChannelManagement = model.PERMISSIONS_ALL *utils.Cfg.TeamSettings.RestrictPublicChannelManagement = model.PERMISSIONS_ALL
*utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = model.PERMISSIONS_ALL *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = model.PERMISSIONS_ALL
utils.SetDefaultRolesBasedOnConfig() utils.SetDefaultRolesBasedOnConfig()
utils.IsLicensed = true 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} 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} 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 isLicensed := utils.IsLicensed
license := utils.License
restrictPublicChannel := *utils.Cfg.TeamSettings.RestrictPublicChannelManagement restrictPublicChannel := *utils.Cfg.TeamSettings.RestrictPublicChannelManagement
restrictPrivateChannel := *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement restrictPrivateChannel := *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement
defer func() { defer func() {
*utils.Cfg.TeamSettings.RestrictPublicChannelManagement = restrictPublicChannel *utils.Cfg.TeamSettings.RestrictPublicChannelManagement = restrictPublicChannel
*utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = restrictPrivateChannel *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = restrictPrivateChannel
utils.IsLicensed = isLicensed utils.IsLicensed = isLicensed
utils.License = license
utils.SetDefaultRolesBasedOnConfig() utils.SetDefaultRolesBasedOnConfig()
}() }()
*utils.Cfg.TeamSettings.RestrictPublicChannelManagement = model.PERMISSIONS_ALL *utils.Cfg.TeamSettings.RestrictPublicChannelManagement = model.PERMISSIONS_ALL
*utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = model.PERMISSIONS_ALL *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = model.PERMISSIONS_ALL
utils.IsLicensed = true utils.IsLicensed = true
utils.License = &model.License{Features: &model.Features{}}
utils.License.Features.SetDefaults()
utils.SetDefaultRolesBasedOnConfig() utils.SetDefaultRolesBasedOnConfig()
channel2 := th.CreateChannel(Client, team) channel2 := th.CreateChannel(Client, team)
@@ -436,17 +444,21 @@ func TestUpdateChannelHeader(t *testing.T) {
} }
isLicensed := utils.IsLicensed isLicensed := utils.IsLicensed
license := utils.License
restrictPublicChannel := *utils.Cfg.TeamSettings.RestrictPublicChannelManagement restrictPublicChannel := *utils.Cfg.TeamSettings.RestrictPublicChannelManagement
restrictPrivateChannel := *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement restrictPrivateChannel := *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement
defer func() { defer func() {
*utils.Cfg.TeamSettings.RestrictPublicChannelManagement = restrictPublicChannel *utils.Cfg.TeamSettings.RestrictPublicChannelManagement = restrictPublicChannel
*utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = restrictPrivateChannel *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = restrictPrivateChannel
utils.IsLicensed = isLicensed utils.IsLicensed = isLicensed
utils.License = license
utils.SetDefaultRolesBasedOnConfig() utils.SetDefaultRolesBasedOnConfig()
}() }()
*utils.Cfg.TeamSettings.RestrictPublicChannelManagement = model.PERMISSIONS_ALL *utils.Cfg.TeamSettings.RestrictPublicChannelManagement = model.PERMISSIONS_ALL
*utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = model.PERMISSIONS_ALL *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = model.PERMISSIONS_ALL
utils.IsLicensed = true utils.IsLicensed = true
utils.License = &model.License{Features: &model.Features{}}
utils.License.Features.SetDefaults()
utils.SetDefaultRolesBasedOnConfig() utils.SetDefaultRolesBasedOnConfig()
th.LoginBasic() th.LoginBasic()
@@ -566,17 +578,21 @@ func TestUpdateChannelPurpose(t *testing.T) {
} }
isLicensed := utils.IsLicensed isLicensed := utils.IsLicensed
license := utils.License
restrictPublicChannel := *utils.Cfg.TeamSettings.RestrictPublicChannelManagement restrictPublicChannel := *utils.Cfg.TeamSettings.RestrictPublicChannelManagement
restrictPrivateChannel := *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement restrictPrivateChannel := *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement
defer func() { defer func() {
*utils.Cfg.TeamSettings.RestrictPublicChannelManagement = restrictPublicChannel *utils.Cfg.TeamSettings.RestrictPublicChannelManagement = restrictPublicChannel
*utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = restrictPrivateChannel *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = restrictPrivateChannel
utils.IsLicensed = isLicensed utils.IsLicensed = isLicensed
utils.License = license
utils.SetDefaultRolesBasedOnConfig() utils.SetDefaultRolesBasedOnConfig()
}() }()
*utils.Cfg.TeamSettings.RestrictPublicChannelManagement = model.PERMISSIONS_ALL *utils.Cfg.TeamSettings.RestrictPublicChannelManagement = model.PERMISSIONS_ALL
*utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = model.PERMISSIONS_ALL *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = model.PERMISSIONS_ALL
utils.IsLicensed = true utils.IsLicensed = true
utils.License = &model.License{Features: &model.Features{}}
utils.License.Features.SetDefaults()
utils.SetDefaultRolesBasedOnConfig() utils.SetDefaultRolesBasedOnConfig()
th.LoginBasic() th.LoginBasic()
@@ -1071,17 +1087,21 @@ func TestDeleteChannel(t *testing.T) {
} }
isLicensed := utils.IsLicensed isLicensed := utils.IsLicensed
license := utils.License
restrictPublicChannel := *utils.Cfg.TeamSettings.RestrictPublicChannelManagement restrictPublicChannel := *utils.Cfg.TeamSettings.RestrictPublicChannelManagement
restrictPrivateChannel := *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement restrictPrivateChannel := *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement
defer func() { defer func() {
*utils.Cfg.TeamSettings.RestrictPublicChannelManagement = restrictPublicChannel *utils.Cfg.TeamSettings.RestrictPublicChannelManagement = restrictPublicChannel
*utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = restrictPrivateChannel *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = restrictPrivateChannel
utils.IsLicensed = isLicensed utils.IsLicensed = isLicensed
utils.License = license
utils.SetDefaultRolesBasedOnConfig() utils.SetDefaultRolesBasedOnConfig()
}() }()
*utils.Cfg.TeamSettings.RestrictPublicChannelManagement = model.PERMISSIONS_ALL *utils.Cfg.TeamSettings.RestrictPublicChannelManagement = model.PERMISSIONS_ALL
*utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = model.PERMISSIONS_ALL *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = model.PERMISSIONS_ALL
utils.IsLicensed = true utils.IsLicensed = true
utils.License = &model.License{Features: &model.Features{}}
utils.License.Features.SetDefaults()
utils.SetDefaultRolesBasedOnConfig() utils.SetDefaultRolesBasedOnConfig()
th.LoginSystemAdmin() th.LoginSystemAdmin()

Просмотреть файл

@@ -47,51 +47,55 @@ type Context struct {
} }
func ApiAppHandler(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { type handler struct {
@@ -102,6 +106,7 @@ type handler struct {
isUserActivity bool isUserActivity bool
isTeamIndependent bool isTeamIndependent bool
trustRequester bool trustRequester bool
requireMfa bool
} }
func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 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() c.UserRequired()
} }
if c.Err == nil && h.requireMfa {
c.MfaRequired()
}
if c.Err == nil && h.requireSystemAdmin { if c.Err == nil && h.requireSystemAdmin {
c.SystemAdminRequired() 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() { func (c *Context) SystemAdminRequired() {
if len(c.Session.UserId) == 0 { if len(c.Session.UserId) == 0 {
c.Err = model.NewLocAppError("", "api.context.session_expired.app_error", nil, "SystemAdminRequired") c.Err = model.NewLocAppError("", "api.context.session_expired.app_error", nil, "SystemAdminRequired")

Просмотреть файл

@@ -856,7 +856,7 @@ func CompleteSwitchWithOAuth(c *Context, w http.ResponseWriter, r *http.Request,
return 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 c.Err = result.Err
return return
} }

Просмотреть файл

@@ -426,10 +426,14 @@ func TestInviteMembers(t *testing.T) {
} }
isLicensed := utils.IsLicensed isLicensed := utils.IsLicensed
license := utils.License
defer func() { defer func() {
utils.IsLicensed = isLicensed utils.IsLicensed = isLicensed
utils.License = license
}() }()
utils.IsLicensed = true utils.IsLicensed = true
utils.License = &model.License{Features: &model.Features{}}
utils.License.Features.SetDefaults()
if _, err := Client.InviteMembers(invites); err == nil { if _, err := Client.InviteMembers(invites); err == nil {
t.Fatal("should have errored not team admin and licensed") t.Fatal("should have errored not team admin and licensed")

Просмотреть файл

@@ -65,8 +65,8 @@ func InitUser() {
BaseRoutes.NeedChannel.Handle("/users/autocomplete", ApiUserRequired(autocompleteUsersInChannel)).Methods("GET") BaseRoutes.NeedChannel.Handle("/users/autocomplete", ApiUserRequired(autocompleteUsersInChannel)).Methods("GET")
BaseRoutes.Users.Handle("/mfa", ApiAppHandler(checkMfa)).Methods("POST") BaseRoutes.Users.Handle("/mfa", ApiAppHandler(checkMfa)).Methods("POST")
BaseRoutes.Users.Handle("/generate_mfa_secret", ApiUserRequiredTrustRequester(generateMfaSecret)).Methods("GET") BaseRoutes.Users.Handle("/generate_mfa_secret", ApiUserRequiredMfa(generateMfaSecret)).Methods("GET")
BaseRoutes.Users.Handle("/update_mfa", ApiUserRequired(updateMfa)).Methods("POST") BaseRoutes.Users.Handle("/update_mfa", ApiUserRequiredMfa(updateMfa)).Methods("POST")
BaseRoutes.Users.Handle("/claim/email_to_oauth", ApiAppHandler(emailToOAuth)).Methods("POST") BaseRoutes.Users.Handle("/claim/email_to_oauth", ApiAppHandler(emailToOAuth)).Methods("POST")
BaseRoutes.Users.Handle("/claim/oauth_to_email", ApiUserRequired(oauthToEmail)).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) { func sendEmailChangeEmail(c *Context, oldEmail, newEmail, siteURL string) {
subject := fmt.Sprintf("[%v] %v", utils.Cfg.TeamSettings.SiteName, c.T("api.templates.email_change_subject", subject := fmt.Sprintf("[%v] %v", utils.Cfg.TeamSettings.SiteName, c.T("api.templates.email_change_subject",
map[string]interface{}{"TeamDisplayName": utils.Cfg.TeamSettings.SiteName})) map[string]interface{}{"TeamDisplayName": utils.Cfg.TeamSettings.SiteName}))
@@ -2016,6 +2040,8 @@ func emailToOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
return return
} }
mfaToken := props["token"]
service := props["service"] service := props["service"]
if len(service) == 0 { if len(service) == 0 {
c.SetInvalidParam("emailToOAuth", "service") c.SetInvalidParam("emailToOAuth", "service")
@@ -2039,7 +2065,7 @@ func emailToOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
user = result.Data.(*model.User) 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.LogAuditWithUserId(user.Id, "failed - bad authentication")
c.Err = err c.Err = err
return return
@@ -2147,6 +2173,8 @@ func emailToLdap(c *Context, w http.ResponseWriter, r *http.Request) {
return return
} }
token := props["token"]
c.LogAudit("attempt") c.LogAudit("attempt")
var user *model.User var user *model.User
@@ -2158,7 +2186,7 @@ func emailToLdap(c *Context, w http.ResponseWriter, r *http.Request) {
user = result.Data.(*model.User) 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.LogAuditWithUserId(user.Id, "failed - bad authentication")
c.Err = err c.Err = err
return return
@@ -2213,6 +2241,8 @@ func ldapToEmail(c *Context, w http.ResponseWriter, r *http.Request) {
return return
} }
token := props["token"]
c.LogAudit("attempt") c.LogAudit("attempt")
var user *model.User var user *model.User
@@ -2242,6 +2272,12 @@ func ldapToEmail(c *Context, w http.ResponseWriter, r *http.Request) {
return 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 { if result := <-Srv.Store.User().UpdatePassword(user.Id, model.HashPassword(emailPassword)); result.Err != nil {
c.LogAudit("fail - database issue") c.LogAudit("fail - database issue")
c.Err = result.Err c.Err = result.Err
@@ -2379,18 +2415,33 @@ func updateMfa(c *Context, w http.ResponseWriter, r *http.Request) {
} }
} }
c.LogAudit("attempt")
if activate { if activate {
if err := ActivateMfa(c.Session.UserId, token); err != nil { if err := ActivateMfa(c.Session.UserId, token); err != nil {
c.Err = err c.Err = err
return return
} }
c.LogAudit("success - activated")
} else { } else {
if err := DeactivateMfa(c.Session.UserId); err != nil { if err := DeactivateMfa(c.Session.UserId); err != nil {
c.Err = err c.Err = err
return 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 := map[string]string{}
rdata["status"] = "ok" rdata["status"] = "ok"
w.Write([]byte(model.MapToJson(rdata))) w.Write([]byte(model.MapToJson(rdata)))

Просмотреть файл

@@ -1341,7 +1341,7 @@ func TestResetPassword(t *testing.T) {
} }
authData := model.NewId() 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) t.Fatal(result.Err)
} }

Просмотреть файл

@@ -25,6 +25,7 @@
"EnableSecurityFixAlert": true, "EnableSecurityFixAlert": true,
"EnableInsecureOutgoingConnections": false, "EnableInsecureOutgoingConnections": false,
"EnableMultifactorAuthentication": false, "EnableMultifactorAuthentication": false,
"EnforceMultifactorAuthentication": false,
"AllowCorsFrom": "", "AllowCorsFrom": "",
"SessionLengthWebInDays": 30, "SessionLengthWebInDays": 30,
"SessionLengthMobileInDays": 30, "SessionLengthMobileInDays": 30,

Просмотреть файл

@@ -711,6 +711,10 @@
"id": "api.context.session_expired.app_error", "id": "api.context.session_expired.app_error",
"translation": "Invalid or expired session, please login again." "translation": "Invalid or expired session, please login again."
}, },
{
"id": "api.context.mfa_required.app_error",
"translation": "Multi-factor authentication is required on this server."
},
{ {
"id": "api.context.system_permissions.app_error", "id": "api.context.system_permissions.app_error",
"translation": "You do not have the appropriate permissions (system)" "translation": "You do not have the appropriate permissions (system)"
@@ -1987,6 +1991,26 @@
"id": "api.templates.password_change_subject", "id": "api.templates.password_change_subject",
"translation": "Your password has been updated for {{.TeamDisplayName}} on {{ .SiteName }}" "translation": "Your password has been updated for {{.TeamDisplayName}} on {{ .SiteName }}"
}, },
{
"id": "api.templates.mfa_activated_body.info",
"translation": "Multi-factor authentication has been added to your account on {{ .SiteURL }}.<br>If this change wasn't initiated by you, please contact your system administrator."
},
{
"id": "api.templates.mfa_deactivated_body.info",
"translation": "Multi-factor authentication has been removed from your account on {{ .SiteURL }}.<br>If this change wasn't initiated by you, please contact your system administrator."
},
{
"id": "api.templates.mfa_activated_body.title",
"translation": "Multi-factor authentication was added"
},
{
"id": "api.templates.mfa_deactivated_body.title",
"translation": "Multi-factor authentication was removed"
},
{
"id": "api.templates.mfa_change_subject",
"translation": "Your MFA has been updated on {{ .SiteName }}"
},
{ {
"id": "api.templates.post_body.button", "id": "api.templates.post_body.button",
"translation": "Go To Post" "translation": "Go To Post"

Просмотреть файл

@@ -80,6 +80,7 @@ type ServiceSettings struct {
EnableSecurityFixAlert *bool EnableSecurityFixAlert *bool
EnableInsecureOutgoingConnections *bool EnableInsecureOutgoingConnections *bool
EnableMultifactorAuthentication *bool EnableMultifactorAuthentication *bool
EnforceMultifactorAuthentication *bool
AllowCorsFrom *string AllowCorsFrom *string
SessionLengthWebInDays *int SessionLengthWebInDays *int
SessionLengthMobileInDays *int SessionLengthMobileInDays *int
@@ -434,6 +435,11 @@ func (o *Config) SetDefaults() {
*o.ServiceSettings.EnableMultifactorAuthentication = false *o.ServiceSettings.EnableMultifactorAuthentication = false
} }
if o.ServiceSettings.EnforceMultifactorAuthentication == nil {
o.ServiceSettings.EnforceMultifactorAuthentication = new(bool)
*o.ServiceSettings.EnforceMultifactorAuthentication = false
}
if o.PasswordSettings.MinimumLength == nil { if o.PasswordSettings.MinimumLength == nil {
o.PasswordSettings.MinimumLength = new(int) o.PasswordSettings.MinimumLength = new(int)
*o.PasswordSettings.MinimumLength = PASSWORD_MINIMUM_LENGTH *o.PasswordSettings.MinimumLength = PASSWORD_MINIMUM_LENGTH

Просмотреть файл

@@ -275,7 +275,7 @@ func (us SqlUserStore) UpdateFailedPasswordAttempts(userId string, attempts int)
return storeChannel return storeChannel
} }
func (us SqlUserStore) UpdateAuthData(userId string, service string, authData *string, email string) StoreChannel { func (us SqlUserStore) UpdateAuthData(userId string, service string, authData *string, email string, resetMfa bool) StoreChannel {
storeChannel := make(StoreChannel, 1) storeChannel := make(StoreChannel, 1)
@@ -301,6 +301,10 @@ func (us SqlUserStore) UpdateAuthData(userId string, service string, authData *s
query += ", Email = :Email" query += ", Email = :Email"
} }
if resetMfa {
query += ", MfaActive = false, MfaSecret = ''"
}
query += " WHERE Id = :UserId" query += " WHERE Id = :UserId"
if _, err := us.GetMaster().Exec(query, map[string]interface{}{"LastPasswordUpdate": updateAt, "UpdateAt": updateAt, "UserId": userId, "AuthService": service, "AuthData": authData, "Email": email}); err != nil { if _, err := us.GetMaster().Exec(query, map[string]interface{}{"LastPasswordUpdate": updateAt, "UpdateAt": updateAt, "UserId": userId, "AuthService": service, "AuthData": authData, "Email": email}); err != nil {

Просмотреть файл

@@ -756,7 +756,7 @@ func TestUserStoreUpdateAuthData(t *testing.T) {
service := "someservice" service := "someservice"
authData := model.NewId() authData := model.NewId()
if err := (<-store.User().UpdateAuthData(u1.Id, service, &authData, "")).Err; err != nil { if err := (<-store.User().UpdateAuthData(u1.Id, service, &authData, "", true)).Err; err != nil {
t.Fatal(err) t.Fatal(err)
} }

Просмотреть файл

@@ -143,7 +143,7 @@ type UserStore interface {
UpdateLastPictureUpdate(userId string) StoreChannel UpdateLastPictureUpdate(userId string) StoreChannel
UpdateUpdateAt(userId string) StoreChannel UpdateUpdateAt(userId string) StoreChannel
UpdatePassword(userId, newPassword string) StoreChannel UpdatePassword(userId, newPassword string) StoreChannel
UpdateAuthData(userId string, service string, authData *string, email string) StoreChannel UpdateAuthData(userId string, service string, authData *string, email string, resetMfa bool) StoreChannel
UpdateMfaSecret(userId, secret string) StoreChannel UpdateMfaSecret(userId, secret string) StoreChannel
UpdateMfaActive(userId string, active bool) StoreChannel UpdateMfaActive(userId string, active bool) StoreChannel
Get(id string) StoreChannel Get(id string) StoreChannel

43
templates/mfa_change_body.html Обычный файл
Просмотреть файл

@@ -0,0 +1,43 @@
{{define "mfa_change_body"}}
<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="margin-top: 20px; line-height: 1.7; color: #555;">
<tr>
<td>
<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 660px; font-family: Helvetica, Arial, sans-serif; font-size: 14px; background: #FFF;">
<tr>
<td style="border: 1px solid #ddd;">
<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;">
<tr>
<td style="padding: 20px 20px 10px; text-align:left;">
<img src="{{.Props.SiteURL}}/static/images/logo-email.png" width="130px" style="opacity: 0.5" alt="">
</td>
</tr>
<tr>
<td>
<table border="0" cellpadding="0" cellspacing="0" style="padding: 20px 50px 0; text-align: center; margin: 0 auto">
<tr>
<td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;">
<h2 style="font-weight: normal; margin-top: 10px;">{{.Props.Title}}</h2>
<p>{{.Html.Info}}</p>
</td>
</tr>
<tr>
{{template "email_info" . }}
</tr>
</table>
</td>
</tr>
<tr>
{{template "email_footer" . }}
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
{{end}}

Просмотреть файл

@@ -311,6 +311,7 @@ func getClientConfig(c *model.Config) map[string]string {
if *License.Features.MFA { if *License.Features.MFA {
props["EnableMultifactorAuthentication"] = strconv.FormatBool(*c.ServiceSettings.EnableMultifactorAuthentication) props["EnableMultifactorAuthentication"] = strconv.FormatBool(*c.ServiceSettings.EnableMultifactorAuthentication)
props["EnforceMultifactorAuthentication"] = strconv.FormatBool(*c.ServiceSettings.EnforceMultifactorAuthentication)
} }
if *License.Features.Compliance { if *License.Features.Compliance {

Просмотреть файл

@@ -16,10 +16,11 @@ import Client from 'client/web_client.jsx';
import {ActionTypes, Preferences} from 'utils/constants.jsx'; import {ActionTypes, Preferences} from 'utils/constants.jsx';
export function switchFromLdapToEmail(email, password, ldapPassword, onSuccess, onError) { export function switchFromLdapToEmail(email, password, token, ldapPassword, onSuccess, onError) {
Client.ldapToEmail( Client.ldapToEmail(
email, email,
password, password,
token,
ldapPassword, ldapPassword,
(data) => { (data) => {
if (data.follow_link) { if (data.follow_link) {
@@ -391,3 +392,43 @@ export function updateUserRoles(userId, newRoles, success, error) {
} }
); );
} }
export function activateMfa(code, success, error) {
Client.updateMfa(
code,
true,
() => {
AsyncClient.getMe();
if (success) {
success();
}
},
(err) => {
if (error) {
error(err);
}
}
);
}
export function checkMfa(loginId, success, error) {
if (global.window.mm_config.EnableMultifactorAuthentication !== 'true') {
success(false);
return;
}
Client.checkMfa(
loginId,
(data) => {
if (success) {
success(data.mfa_required === 'true');
}
},
(err) => {
if (error) {
error(err);
}
}
);
}

Просмотреть файл

@@ -840,18 +840,13 @@ export default class Client {
this.track('api', 'api_users_reset_password'); this.track('api', 'api_users_reset_password');
} }
emailToOAuth(email, password, service, success, error) { emailToOAuth(email, password, token, service, success, error) {
var data = {};
data.password = password;
data.email = email;
data.service = service;
request. request.
post(`${this.getUsersRoute()}/claim/email_to_oauth`). post(`${this.getUsersRoute()}/claim/email_to_oauth`).
set(this.defaultHeaders). set(this.defaultHeaders).
type('application/json'). type('application/json').
accept('application/json'). accept('application/json').
send(data). send({password, email, token, service}).
end(this.handleResponse.bind(this, 'emailToOAuth', success, error)); end(this.handleResponse.bind(this, 'emailToOAuth', success, error));
this.track('api', 'api_users_email_to_oauth'); this.track('api', 'api_users_email_to_oauth');
@@ -873,12 +868,13 @@ export default class Client {
this.track('api', 'api_users_oauth_to_email'); this.track('api', 'api_users_oauth_to_email');
} }
emailToLdap(email, password, ldapId, ldapPassword, success, error) { emailToLdap(email, password, token, ldapId, ldapPassword, success, error) {
var data = {}; var data = {};
data.email_password = password; data.email_password = password;
data.email = email; data.email = email;
data.ldap_id = ldapId; data.ldap_id = ldapId;
data.ldap_password = ldapPassword; data.ldap_password = ldapPassword;
data.token = token;
request. request.
post(`${this.getUsersRoute()}/claim/email_to_ldap`). post(`${this.getUsersRoute()}/claim/email_to_ldap`).
@@ -891,11 +887,12 @@ export default class Client {
this.track('api', 'api_users_email_to_ldap'); this.track('api', 'api_users_email_to_ldap');
} }
ldapToEmail(email, emailPassword, ldapPassword, success, error) { ldapToEmail(email, emailPassword, token, ldapPassword, success, error) {
var data = {}; var data = {};
data.email = email; data.email = email;
data.ldap_password = ldapPassword; data.ldap_password = ldapPassword;
data.email_password = emailPassword; data.email_password = emailPassword;
data.token = token;
request. request.
post(`${this.getUsersRoute()}/claim/ldap_to_email`). post(`${this.getUsersRoute()}/claim/ldap_to_email`).

Просмотреть файл

@@ -38,6 +38,11 @@ class WebClientClass extends Client {
} }
handleError(err, res) { handleError(err, res) {
if (res.body.id === 'api.context.mfa_required.app_error') {
window.location.reload();
return;
}
if (err.status === HTTP_UNAUTHORIZED && res.req.url !== '/api/v3/users/login') { if (err.status === HTTP_UNAUTHORIZED && res.req.url !== '/api/v3/users/login') {
GlobalActions.emitUserLoggedOutEvent('/login'); GlobalActions.emitUserLoggedOutEvent('/login');
} }

Просмотреть файл

@@ -194,6 +194,7 @@ export default class AdminSidebar extends React.Component {
let clusterSettings = null; let clusterSettings = null;
let metricsSettings = null; let metricsSettings = null;
let complianceSettings = null; let complianceSettings = null;
let mfaSettings = null;
let license = null; let license = null;
let audits = null; let audits = null;
@@ -284,6 +285,20 @@ export default class AdminSidebar extends React.Component {
); );
} }
if (global.window.mm_license.MFA === 'true') {
mfaSettings = (
<AdminSidebarSection
name='mfa'
title={
<FormattedMessage
id='admin.sidebar.mfa'
defaultMessage='MFA'
/>
}
/>
);
}
oauthSettings = ( oauthSettings = (
<AdminSidebarSection <AdminSidebarSection
name='oauth' name='oauth'
@@ -507,6 +522,7 @@ export default class AdminSidebar extends React.Component {
{oauthSettings} {oauthSettings}
{ldapSettings} {ldapSettings}
{samlSettings} {samlSettings}
{mfaSettings}
</AdminSidebarSection> </AdminSidebarSection>
<AdminSidebarSection <AdminSidebarSection
name='security' name='security'

Просмотреть файл

@@ -0,0 +1,99 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import AdminSettings from './admin_settings.jsx';
import SettingsGroup from './settings_group.jsx';
import BooleanSetting from './boolean_setting.jsx';
import React from 'react';
import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
export default class MfaSettings extends AdminSettings {
constructor(props) {
super(props);
this.getConfigFromState = this.getConfigFromState.bind(this);
this.renderSettings = this.renderSettings.bind(this);
this.state = Object.assign(this.state, {
enableMultifactorAuthentication: props.config.ServiceSettings.EnableMultifactorAuthentication,
enforceMultifactorAuthentication: props.config.ServiceSettings.EnforceMultifactorAuthentication
});
}
getConfigFromState(config) {
config.ServiceSettings.EnableMultifactorAuthentication = this.state.enableMultifactorAuthentication;
config.ServiceSettings.EnforceMultifactorAuthentication = this.state.enableMultifactorAuthentication && this.state.enforceMultifactorAuthentication;
return config;
}
getStateFromConfig(config) {
return {
enableMultifactorAuthentication: config.ServiceSettings.EnableMultifactorAuthentication,
enforceMultifactorAuthentication: config.ServiceSettings.EnableMultifactorAuthentication && config.ServiceSettings.EnforceMultifactorAuthentication
};
}
renderTitle() {
return (
<h3>
<FormattedMessage
id='admin.mfa.title'
defaultMessage='Multi-factor Authentication'
/>
</h3>
);
}
renderSettings() {
return (
<SettingsGroup>
<div className='banner'>
<div className='banner__content'>
<FormattedMessage
id='admin.mfa.bannerDesc'
defaultMessage='Multi-factor authentication is only available for accounts with LDAP and email login methods. If there are users on your system with other login methods, it is recommended you set up multi-factor authentication directly with the SSO or SAML provider.'
/>
</div>
</div>
<BooleanSetting
id='enableMultifactorAuthentication'
label={
<FormattedMessage
id='admin.service.mfaTitle'
defaultMessage='Enable Multi-factor Authentication:'
/>
}
helpText={
<FormattedMessage
id='admin.service.mfaDesc'
defaultMessage='When true, users will be given the option to add multi-factor authentication to their account. They will need a smartphone and an authenticator app such as Google Authenticator.'
/>
}
value={this.state.enableMultifactorAuthentication}
onChange={this.handleChange}
/>
<BooleanSetting
id='enforceMultifactorAuthentication'
label={
<FormattedMessage
id='admin.service.enforceMfaTitle'
defaultMessage='Enforce Multi-factor Authentication:'
/>
}
helpText={
<FormattedHTMLMessage
id='admin.service.enforceMfaDesc'
defaultMessage='When true, users on the system will be required to set up [multi-factor authentication]. Any logged in users will be redirected to the multi-factor authentication setup page until they successfully add MFA to their account.<br/><br/>It is recommended you turn on enforcement during non-peak hours, when people are less likely to be using the system. New users will be required to set up multi-factor authentication when they first sign up. After set up, users will not be able to remove multi-factor authentication unless enforcement is disabled.<br/><br/>Please note that multi-factor authentication is only available for accounts with LDAP and email login methods. Mattermost will not enforce multi-factor authentication for other login methods. If there are users on your system using other login methods, it is recommended you set up and enforce multi-factor authentication directly with the SSO or SAML provider.'
/>
}
disabled={!this.state.enableMultifactorAuthentication}
value={this.state.enforceMultifactorAuthentication}
onChange={this.handleChange}
/>
</SettingsGroup>
);
}
}

Просмотреть файл

@@ -6,7 +6,6 @@ import AdminSettings from './admin_settings.jsx';
import {FormattedMessage} from 'react-intl'; import {FormattedMessage} from 'react-intl';
import SettingsGroup from './settings_group.jsx'; import SettingsGroup from './settings_group.jsx';
import TextSetting from './text_setting.jsx'; import TextSetting from './text_setting.jsx';
import BooleanSetting from './boolean_setting.jsx';
import Setting from './setting.jsx'; import Setting from './setting.jsx';
import * as Utils from 'utils/utils.jsx'; import * as Utils from 'utils/utils.jsx';
import Constants from 'utils/constants.jsx'; import Constants from 'utils/constants.jsx';
@@ -32,7 +31,6 @@ export default class PasswordSettings extends AdminSettings {
passwordUppercase: props.config.PasswordSettings.Uppercase, passwordUppercase: props.config.PasswordSettings.Uppercase,
passwordSymbol: props.config.PasswordSettings.Symbol, passwordSymbol: props.config.PasswordSettings.Symbol,
maximumLoginAttempts: props.config.ServiceSettings.MaximumLoginAttempts, maximumLoginAttempts: props.config.ServiceSettings.MaximumLoginAttempts,
enableMultifactorAuthentication: props.config.ServiceSettings.EnableMultifactorAuthentication,
passwordResetSalt: props.config.EmailSettings.PasswordResetSalt passwordResetSalt: props.config.EmailSettings.PasswordResetSalt
}); });
@@ -75,9 +73,6 @@ export default class PasswordSettings extends AdminSettings {
config.ServiceSettings.MaximumLoginAttempts = this.parseIntNonZero(this.state.maximumLoginAttempts); config.ServiceSettings.MaximumLoginAttempts = this.parseIntNonZero(this.state.maximumLoginAttempts);
config.EmailSettings.PasswordResetSalt = this.state.passwordResetSalt; config.EmailSettings.PasswordResetSalt = this.state.passwordResetSalt;
if (global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.MFA === 'true') {
config.ServiceSettings.EnableMultifactorAuthentication = this.state.enableMultifactorAuthentication;
}
return config; return config;
} }
@@ -90,7 +85,6 @@ export default class PasswordSettings extends AdminSettings {
passwordUppercase: config.PasswordSettings.Uppercase, passwordUppercase: config.PasswordSettings.Uppercase,
passwordSymbol: config.PasswordSettings.Symbol, passwordSymbol: config.PasswordSettings.Symbol,
maximumLoginAttempts: config.ServiceSettings.MaximumLoginAttempts, maximumLoginAttempts: config.ServiceSettings.MaximumLoginAttempts,
enableMultifactorAuthentication: config.ServiceSettings.EnableMultifactorAuthentication,
passwordResetSalt: config.EmailSettings.PasswordResetSalt passwordResetSalt: config.EmailSettings.PasswordResetSalt
}; };
} }
@@ -154,29 +148,6 @@ export default class PasswordSettings extends AdminSettings {
} }
renderSettings() { renderSettings() {
let mfaSetting = null;
if (global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.MFA === 'true') {
mfaSetting = (
<BooleanSetting
id='enableMultifactorAuthentication'
label={
<FormattedMessage
id='admin.service.mfaTitle'
defaultMessage='Enable Multi-factor Authentication:'
/>
}
helpText={
<FormattedMessage
id='admin.service.mfaDesc'
defaultMessage='When true, users will be given the option to add multi-factor authentication to their account. They will need a smartphone and an authenticator app such as Google Authenticator.'
/>
}
value={this.state.enableMultifactorAuthentication}
onChange={this.handleChange}
/>
);
}
let passwordSettings = null; let passwordSettings = null;
if (global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.PasswordRequirements === 'true') { if (global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.PasswordRequirements === 'true') {
passwordSettings = ( passwordSettings = (
@@ -332,8 +303,7 @@ export default class PasswordSettings extends AdminSettings {
value={this.state.maximumLoginAttempts} value={this.state.maximumLoginAttempts}
onChange={this.handleChange} onChange={this.handleChange}
/> />
{mfaSetting}
</SettingsGroup> </SettingsGroup>
); );
} }
} }

Просмотреть файл

@@ -1,11 +1,14 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
import LoginMfa from 'components/login/components/login_mfa.jsx';
import * as Utils from 'utils/utils.jsx'; import * as Utils from 'utils/utils.jsx';
import Client from 'client/web_client.jsx'; import Client from 'client/web_client.jsx';
import {checkMfa} from 'actions/user_actions.jsx';
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom';
import {FormattedMessage} from 'react-intl'; import {FormattedMessage} from 'react-intl';
export default class EmailToLDAP extends React.Component { export default class EmailToLDAP extends React.Component {
@@ -13,16 +16,20 @@ export default class EmailToLDAP extends React.Component {
super(props); super(props);
this.submit = this.submit.bind(this); this.submit = this.submit.bind(this);
this.preSubmit = this.preSubmit.bind(this);
this.state = { this.state = {
passwordError: '', passwordError: '',
ldapError: '', ldapError: '',
ldapPasswordError: '', ldapPasswordError: '',
serverError: '' serverError: '',
showMfa: false
}; };
} }
submit(e) {
preSubmit(e) {
e.preventDefault(); e.preventDefault();
var state = { var state = {
passwordError: '', passwordError: '',
ldapError: '', ldapError: '',
@@ -30,44 +37,65 @@ export default class EmailToLDAP extends React.Component {
serverError: '' serverError: ''
}; };
const password = ReactDOM.findDOMNode(this.refs.emailpassword).value; const password = this.refs.emailpassword.value;
if (!password) { if (!password) {
state.passwordError = Utils.localizeMessage('claim.email_to_ldap.pwdError', 'Please enter your password.'); state.passwordError = Utils.localizeMessage('claim.email_to_ldap.pwdError', 'Please enter your password.');
this.setState(state); this.setState(state);
return; return;
} }
const ldapId = ReactDOM.findDOMNode(this.refs.ldapid).value.trim(); const ldapId = this.refs.ldapid.value.trim();
if (!ldapId) { if (!ldapId) {
state.ldapError = Utils.localizeMessage('claim.email_to_ldap.ldapIdError', 'Please enter your AD/LDAP ID.'); state.ldapError = Utils.localizeMessage('claim.email_to_ldap.ldapIdError', 'Please enter your AD/LDAP ID.');
this.setState(state); this.setState(state);
return; return;
} }
const ldapPassword = ReactDOM.findDOMNode(this.refs.ldappassword).value; const ldapPassword = this.refs.ldappassword.value;
if (!ldapPassword) { if (!ldapPassword) {
state.ldapPasswordError = Utils.localizeMessage('claim.email_to_ldap.ldapPasswordError', 'Please enter your AD/LDAP password.'); state.ldapPasswordError = Utils.localizeMessage('claim.email_to_ldap.ldapPasswordError', 'Please enter your AD/LDAP password.');
this.setState(state); this.setState(state);
return; return;
} }
state.password = password;
state.ldapId = ldapId;
state.ldapPassword = ldapPassword;
this.setState(state); this.setState(state);
Client.emailToLdap( checkMfa(
this.props.email, this.props.email,
(requiresMfa) => {
if (requiresMfa) {
this.setState({showMfa: true});
} else {
this.submit(this.props.email, password, '', ldapId, ldapPassword);
}
},
(err) => {
this.setState({error: err.message});
}
);
}
submit(loginId, password, token, ldapId, ldapPassword) {
Client.emailToLdap(
loginId,
password, password,
ldapId, token,
ldapPassword, ldapId || this.state.ldapId,
ldapPassword || this.state.ldapPassword,
(data) => { (data) => {
if (data.follow_link) { if (data.follow_link) {
window.location.href = data.follow_link; window.location.href = data.follow_link;
} }
}, },
(err) => { (err) => {
this.setState({serverError: err.message}); this.setState({serverError: err.message, showMfa: false});
} }
); );
} }
render() { render() {
let serverError = null; let serverError = null;
let formClass = 'form-group'; let formClass = 'form-group';
@@ -111,16 +139,19 @@ export default class EmailToLDAP extends React.Component {
passwordPlaceholder = Utils.localizeMessage('claim.email_to_ldap.ldapPwd', 'AD/LDAP Password'); passwordPlaceholder = Utils.localizeMessage('claim.email_to_ldap.ldapPwd', 'AD/LDAP Password');
} }
return ( let content;
<div> if (this.state.showMfa) {
<h3> content = (
<FormattedMessage <LoginMfa
id='claim.email_to_ldap.title' loginId={this.props.email}
defaultMessage='Switch Email/Password Account to AD/LDAP' password={this.state.password}
/> submit={this.submit}
</h3> />
);
} else {
content = (
<form <form
onSubmit={this.submit} onSubmit={this.preSubmit}
className={formClass} className={formClass}
> >
<p> <p>
@@ -202,6 +233,18 @@ export default class EmailToLDAP extends React.Component {
</button> </button>
{serverError} {serverError}
</form> </form>
);
}
return (
<div>
<h3>
<FormattedMessage
id='claim.email_to_ldap.title'
defaultMessage='Switch Email/Password Account to AD/LDAP'
/>
</h3>
{content}
</div> </div>
); );
} }

Просмотреть файл

@@ -1,10 +1,14 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
import LoginMfa from 'components/login/components/login_mfa.jsx';
import * as Utils from 'utils/utils.jsx'; import * as Utils from 'utils/utils.jsx';
import Client from 'client/web_client.jsx'; import Client from 'client/web_client.jsx';
import Constants from 'utils/constants.jsx'; import Constants from 'utils/constants.jsx';
import {checkMfa} from 'actions/user_actions.jsx';
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import {FormattedMessage} from 'react-intl'; import {FormattedMessage} from 'react-intl';
@@ -14,10 +18,12 @@ export default class EmailToOAuth extends React.Component {
super(props); super(props);
this.submit = this.submit.bind(this); this.submit = this.submit.bind(this);
this.preSubmit = this.preSubmit.bind(this);
this.state = {}; this.state = {showMfa: false, password: ''};
} }
submit(e) {
preSubmit(e) {
e.preventDefault(); e.preventDefault();
var state = {}; var state = {};
@@ -28,16 +34,18 @@ export default class EmailToOAuth extends React.Component {
return; return;
} }
this.setState({password});
state.error = null; state.error = null;
this.setState(state); this.setState(state);
Client.emailToOAuth( checkMfa(
this.props.email, this.props.email,
password, (requiresMfa) => {
this.props.newType, if (requiresMfa) {
(data) => { this.setState({showMfa: true});
if (data.follow_link) { } else {
window.location.href = data.follow_link; this.submit(this.props.email, password, '');
} }
}, },
(err) => { (err) => {
@@ -45,6 +53,24 @@ export default class EmailToOAuth extends React.Component {
} }
); );
} }
submit(loginId, password, token) {
Client.emailToOAuth(
loginId,
password,
token,
this.props.newType,
(data) => {
if (data.follow_link) {
window.location.href = data.follow_link;
}
},
(err) => {
this.setState({error: err.message, showMfa: false});
}
);
}
render() { render() {
var error = null; var error = null;
if (this.state.error) { if (this.state.error) {
@@ -59,18 +85,18 @@ export default class EmailToOAuth extends React.Component {
const type = (this.props.newType === Constants.SAML_SERVICE ? Constants.SAML_SERVICE.toUpperCase() : Utils.toTitleCase(this.props.newType)); const type = (this.props.newType === Constants.SAML_SERVICE ? Constants.SAML_SERVICE.toUpperCase() : Utils.toTitleCase(this.props.newType));
const uiType = `${type} SSO`; const uiType = `${type} SSO`;
return ( let content;
<div> if (this.state.showMfa) {
<h3> content = (
<FormattedMessage <LoginMfa
id='claim.email_to_oauth.title' loginId={this.props.email}
defaultMessage='Switch Email/Password Account to {uiType}' password={this.state.password}
values={{ submit={this.submit}
uiType />
}} );
/> } else {
</h3> content = (
<form onSubmit={this.submit}> <form onSubmit={this.preSubmit}>
<p> <p>
<FormattedMessage <FormattedMessage
id='claim.email_to_oauth.ssoType' id='claim.email_to_oauth.ssoType'
@@ -122,6 +148,21 @@ export default class EmailToOAuth extends React.Component {
/> />
</button> </button>
</form> </form>
);
}
return (
<div>
<h3>
<FormattedMessage
id='claim.email_to_oauth.title'
defaultMessage='Switch Email/Password Account to {uiType}'
values={{
uiType
}}
/>
</h3>
{content}
</div> </div>
); );
} }

Просмотреть файл

@@ -1,9 +1,11 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
import LoginMfa from 'components/login/components/login_mfa.jsx';
import * as Utils from 'utils/utils.jsx'; import * as Utils from 'utils/utils.jsx';
import {switchFromLdapToEmail} from 'actions/user_actions.jsx'; import {checkMfa, switchFromLdapToEmail} from 'actions/user_actions.jsx';
import React from 'react'; import React from 'react';
import {FormattedMessage} from 'react-intl'; import {FormattedMessage} from 'react-intl';
@@ -13,6 +15,7 @@ export default class LDAPToEmail extends React.Component {
super(props); super(props);
this.submit = this.submit.bind(this); this.submit = this.submit.bind(this);
this.preSubmit = this.preSubmit.bind(this);
this.state = { this.state = {
passwordError: '', passwordError: '',
@@ -22,8 +25,9 @@ export default class LDAPToEmail extends React.Component {
}; };
} }
submit(e) { preSubmit(e) {
e.preventDefault(); e.preventDefault();
var state = { var state = {
passwordError: '', passwordError: '',
confirmError: '', confirmError: '',
@@ -60,14 +64,33 @@ export default class LDAPToEmail extends React.Component {
return; return;
} }
state.password = password;
state.ldapPassword = ldapPassword;
this.setState(state); this.setState(state);
checkMfa(
this.props.email,
(requiresMfa) => {
if (requiresMfa) {
this.setState({showMfa: true});
} else {
this.submit(this.props.email, password, '', ldapPassword);
}
},
(err) => {
this.setState({error: err.message});
}
);
}
submit(loginId, password, token, ldapPassword) {
switchFromLdapToEmail( switchFromLdapToEmail(
this.props.email, this.props.email,
password, password,
ldapPassword, token,
ldapPassword || this.state.ldapPassword,
null, null,
(err) => this.setState({serverError: err.message}) (err) => this.setState({serverError: err.message, showMfa: false})
); );
} }
@@ -107,16 +130,19 @@ export default class LDAPToEmail extends React.Component {
passwordPlaceholder = Utils.localizeMessage('claim.ldap_to_email.ldapPwd', 'AD/LDAP Password'); passwordPlaceholder = Utils.localizeMessage('claim.ldap_to_email.ldapPwd', 'AD/LDAP Password');
} }
return ( let content;
<div> if (this.state.showMfa) {
<h3> content = (
<FormattedMessage <LoginMfa
id='claim.ldap_to_email.title' loginId={this.props.email}
defaultMessage='Switch AD/LDAP Account to Email/Password' password={this.state.password}
/> submit={this.submit}
</h3> />
);
} else {
content = (
<form <form
onSubmit={this.submit} onSubmit={this.preSubmit}
className={formClass} className={formClass}
> >
<p> <p>
@@ -194,6 +220,18 @@ export default class LDAPToEmail extends React.Component {
</button> </button>
{serverError} {serverError}
</form> </form>
);
}
return (
<div>
<h3>
<FormattedMessage
id='claim.ldap_to_email.title'
defaultMessage='Switch AD/LDAP Account to Email/Password'
/>
</h3>
{content}
</div> </div>
); );
} }

Просмотреть файл

@@ -17,6 +17,7 @@ export default class LoginMfa extends React.Component {
serverError: '' serverError: ''
}; };
} }
handleSubmit(e) { handleSubmit(e) {
e.preventDefault(); e.preventDefault();
const state = {}; const state = {};
@@ -33,6 +34,7 @@ export default class LoginMfa extends React.Component {
this.props.submit(this.props.loginId, this.props.password, token); this.props.submit(this.props.loginId, this.props.password, token);
} }
render() { render() {
let serverError; let serverError;
let errorClass = ''; let errorClass = '';

Просмотреть файл

@@ -0,0 +1,75 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import Constants from 'utils/constants.jsx';
const KeyCodes = Constants.KeyCodes;
import React from 'react';
import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
import {browserHistory} from 'react-router/es6';
export default class Confirm extends React.Component {
constructor(props) {
super(props);
this.onKeyPress = this.onKeyPress.bind(this);
}
componentDidMount() {
document.body.addEventListener('keydown', this.onKeyPress);
}
componentWillUnmount() {
document.body.removeEventListener('keydown', this.onKeyPress);
}
submit(e) {
e.preventDefault();
browserHistory.push('/');
}
onKeyPress(e) {
if (e.which === KeyCodes.ENTER) {
this.submit(e);
}
}
render() {
return (
<div>
<form
onSubmit={this.submit}
onKeyPress={this.onKeyPress}
className='form-group'
>
<p>
<FormattedHTMLMessage
id='mfa.confirm.complete'
defaultMessage='<strong>Set up complete!</strong>'
/>
</p>
<p>
<FormattedMessage
id='mfa.confirm.secure'
defaultMessage='Your account is now secure. Next time you sign in, you will be asked to enter a code from the Google Authenticator app on your phone.'
/>
</p>
<button
type='submit'
className='btn btn-primary'
>
<FormattedMessage
id='mfa.confirm.okay'
defaultMessage='Okay'
/>
</button>
</form>
</div>
);
}
}
Confirm.defaultProps = {
};
Confirm.propTypes = {
};

156
webapp/components/mfa/components/setup.jsx Обычный файл
Просмотреть файл

@@ -0,0 +1,156 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {generateMfaSecret, activateMfa} from 'actions/user_actions.jsx';
import UserStore from 'stores/user_store.jsx';
import * as Utils from 'utils/utils.jsx';
import React from 'react';
import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
import {browserHistory} from 'react-router/es6';
export default class Setup extends React.Component {
constructor(props) {
super(props);
this.submit = this.submit.bind(this);
this.state = {secret: '', qrCode: ''};
}
componentDidMount() {
const user = UserStore.getCurrentUser();
if (!user || user.mfa_active) {
browserHistory.push('/');
return;
}
generateMfaSecret(
(data) => this.setState({secret: data.secret, qrCode: data.qr_code}),
(err) => this.setState({serverError: err.message})
);
}
submit(e) {
e.preventDefault();
const code = this.refs.code.value.replace(/\s/g, '');
if (!code || code.length === 0) {
this.setState({error: Utils.localizeMessage('mfa.setup.codeError', 'Please enter the code from Google Authenticator.')});
return;
}
this.setState({error: null});
activateMfa(
code,
() => {
browserHistory.push('/mfa/confirm');
},
(err) => {
if (err.id === 'ent.mfa.activate.authenticate.app_error') {
this.setState({error: Utils.localizeMessage('mfa.setup.badCode', 'Invalid code. If this issue persists, contact your System Administrator.')});
return;
}
this.setState({error: err.message});
}
);
}
render() {
let formClass = 'form-group';
let errorContent;
if (this.state.error) {
errorContent = <div className='form-group has-error'><label className='control-label'>{this.state.error}</label></div>;
formClass += ' has-error';
}
let mfaRequired;
if (global.window.mm_config.EnforceMultifactorAuthentication) {
mfaRequired = (
<p>
<FormattedHTMLMessage
id='mfa.setup.required'
defaultMessage='<strong>Multi-factor authentication is required on {siteName}.</strong>'
values={{
siteName: global.window.mm_config.SiteName
}}
/>
</p>
);
}
return (
<div>
<form
onSubmit={this.submit}
className={formClass}
>
{mfaRequired}
<p>
<FormattedHTMLMessage
id='mfa.setup.step1'
defaultMessage="<strong>Step 1: </strong>On your phone, download Google Authenticator from <a target='_blank' href='https://itunes.apple.com/us/app/google-authenticator/id388497605?mt=8'>iTunes</a> or <a target='_blank' href='https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en'>Google Play</a>"
/>
</p>
<p>
<FormattedHTMLMessage
id='mfa.setup.step2'
defaultMessage='<strong>Step 2: </strong>Use Google Authenticator to scan this QR code, or manually type in the secret key'
/>
</p>
<div className='form-group'>
<div className='col-sm-12'>
<img
style={{maxHeight: 170}}
src={'data:image/png;base64,' + this.state.qrCode}
/>
</div>
</div>
<br/>
<div className='form-group'>
<p className='col-sm-12'>
<FormattedMessage
id='mfa.setup.secret'
defaultMessage='Secret: {secret}'
values={{
secret: this.state.secret
}}
/>
</p>
</div>
<p>
<FormattedHTMLMessage
id='mfa.setup.step3'
defaultMessage='<strong>Step 3: </strong>Enter the code generated by Google Authenticator'
/>
</p>
<p>
<input
ref='code'
className='form-control'
placeholder={Utils.localizeMessage('mfa.setup.code', 'MFA Code')}
autoFocus={true}
/>
</p>
{errorContent}
<button
type='submit'
className='btn btn-primary'
>
<FormattedMessage
id='mfa.setup.save'
defaultMessage='Save'
/>
</button>
</form>
</div>
);
}
}
Setup.defaultProps = {
};
Setup.propTypes = {
};

66
webapp/components/mfa/mfa_controller.jsx Обычный файл
Просмотреть файл

@@ -0,0 +1,66 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import {FormattedMessage} from 'react-intl';
import {browserHistory, Link} from 'react-router/es6';
import logoImage from 'images/logo.png';
export default class MFAController extends React.Component {
componentDidMount() {
if (window.mm_license.MFA !== 'true' || window.mm_config.EnableMultifactorAuthentication !== 'true') {
browserHistory.push('/');
}
}
render() {
let backButton;
if (window.mm_config.EnforceMultifactorAuthentication !== 'true') {
backButton = (
<div className='signup-header'>
<Link to='/'>
<span className='fa fa-chevron-left'/>
<FormattedMessage
id='web.header.back'
/>
</Link>
</div>
);
}
return (
<div className='inner-wrap sticky'>
<div className='content'>
<div>
{backButton}
<div className='col-sm-12'>
<div className='signup-team__container'>
<h3>
<FormattedMessage
id='mfa.setupTitle'
defaultMessage='Multi-factor Authentication Setup'
/>
</h3>
<img
className='signup-team-logo'
src={logoImage}
/>
<div id='mfa'>
{React.cloneElement(this.props.children, {})}
</div>
</div>
</div>
</div>
</div>
</div>
);
}
}
MFAController.defaultProps = {
};
MFAController.propTypes = {
location: React.PropTypes.object.isRequired,
children: React.PropTypes.node
};

Просмотреть файл

@@ -9,8 +9,6 @@ import ToggleModalButton from '../toggle_modal_button.jsx';
import PreferenceStore from 'stores/preference_store.jsx'; import PreferenceStore from 'stores/preference_store.jsx';
import {generateMfaSecret} from 'actions/user_actions.jsx';
import Client from 'client/web_client.jsx'; import Client from 'client/web_client.jsx';
import * as AsyncClient from 'utils/async_client.jsx'; import * as AsyncClient from 'utils/async_client.jsx';
import * as Utils from 'utils/utils.jsx'; import * as Utils from 'utils/utils.jsx';
@@ -18,8 +16,8 @@ import Constants from 'utils/constants.jsx';
import $ from 'jquery'; import $ from 'jquery';
import React from 'react'; import React from 'react';
import {FormattedMessage, FormattedHTMLMessage, FormattedTime, FormattedDate} from 'react-intl'; import {FormattedMessage, FormattedTime, FormattedDate} from 'react-intl';
import {Link} from 'react-router/es6'; import {browserHistory, Link} from 'react-router/es6';
import icon50 from 'images/icon50x50.png'; import icon50 from 'images/icon50x50.png';
@@ -28,17 +26,15 @@ export default class SecurityTab extends React.Component {
super(props); super(props);
this.submitPassword = this.submitPassword.bind(this); this.submitPassword = this.submitPassword.bind(this);
this.activateMfa = this.activateMfa.bind(this); this.setupMfa = this.setupMfa.bind(this);
this.deactivateMfa = this.deactivateMfa.bind(this); this.deactivateMfa = this.deactivateMfa.bind(this);
this.updateCurrentPassword = this.updateCurrentPassword.bind(this); this.updateCurrentPassword = this.updateCurrentPassword.bind(this);
this.updateNewPassword = this.updateNewPassword.bind(this); this.updateNewPassword = this.updateNewPassword.bind(this);
this.updateConfirmPassword = this.updateConfirmPassword.bind(this); this.updateConfirmPassword = this.updateConfirmPassword.bind(this);
this.updateMfaToken = this.updateMfaToken.bind(this);
this.getDefaultState = this.getDefaultState.bind(this); this.getDefaultState = this.getDefaultState.bind(this);
this.createPasswordSection = this.createPasswordSection.bind(this); this.createPasswordSection = this.createPasswordSection.bind(this);
this.createSignInSection = this.createSignInSection.bind(this); this.createSignInSection = this.createSignInSection.bind(this);
this.createOAuthAppsSection = this.createOAuthAppsSection.bind(this); this.createOAuthAppsSection = this.createOAuthAppsSection.bind(this);
this.showQrCode = this.showQrCode.bind(this);
this.deauthorizeApp = this.deauthorizeApp.bind(this); this.deauthorizeApp = this.deauthorizeApp.bind(this);
this.state = this.getDefaultState(); this.state = this.getDefaultState();
@@ -51,9 +47,7 @@ export default class SecurityTab extends React.Component {
confirmPassword: '', confirmPassword: '',
passwordError: '', passwordError: '',
serverError: '', serverError: '',
authService: this.props.user.auth_service, authService: this.props.user.auth_service
mfaShowQr: false,
mfaToken: ''
}; };
} }
@@ -119,26 +113,9 @@ export default class SecurityTab extends React.Component {
); );
} }
activateMfa() { setupMfa(e) {
Client.updateMfa( e.preventDefault();
this.state.mfaToken, browserHistory.push('/mfa/setup');
true,
() => {
this.props.updateSection('');
AsyncClient.getMe();
this.setState(this.getDefaultState());
},
(err) => {
const state = this.getDefaultState();
if (err.message) {
state.serverError = err.message;
} else {
state.serverError = err;
}
state.mfaError = '';
this.setState(state);
}
);
} }
deactivateMfa() { deactivateMfa() {
@@ -146,6 +123,13 @@ export default class SecurityTab extends React.Component {
'', '',
false, false,
() => { () => {
if (global.window.mm_license.MFA === 'true' &&
global.window.mm_config.EnableMultifactorAuthentication === 'true' &&
global.window.mm_config.EnforceMultifactorAuthentication === 'true') {
window.location.href = '/mfa/setup';
return;
}
this.props.updateSection(''); this.props.updateSection('');
AsyncClient.getMe(); AsyncClient.getMe();
this.setState(this.getDefaultState()); this.setState(this.getDefaultState());
@@ -157,7 +141,6 @@ export default class SecurityTab extends React.Component {
} else { } else {
state.serverError = err; state.serverError = err;
} }
state.mfaError = '';
this.setState(state); this.setState(state);
} }
); );
@@ -175,18 +158,6 @@ export default class SecurityTab extends React.Component {
this.setState({confirmPassword: e.target.value}); this.setState({confirmPassword: e.target.value});
} }
updateMfaToken(e) {
this.setState({mfaToken: e.target.value});
}
showQrCode(e) {
e.preventDefault();
generateMfaSecret(
(data) => this.setState({mfaShowQr: true, secret: data.secret, qrCode: data.qr_code}),
(err) => this.setState({serverError: err.message})
);
}
deauthorizeApp(e) { deauthorizeApp(e) {
e.preventDefault(); e.preventDefault();
const appId = e.currentTarget.getAttribute('data-app'); const appId = e.currentTarget.getAttribute('data-app');
@@ -212,6 +183,39 @@ export default class SecurityTab extends React.Component {
let content; let content;
let extraInfo; let extraInfo;
if (this.props.user.mfa_active) { if (this.props.user.mfa_active) {
let mfaRemoveHelp;
let mfaButtonText;
if (global.window.mm_config.EnforceMultifactorAuthentication === 'true') {
mfaRemoveHelp = (
<FormattedMessage
id='user.settings.mfa.requiredHelp'
defaultMessage='Multi-factor authentication is required on this server. Resetting is only recommended when you need to switch code generation to a new mobile device. You will be required to set it up again immediately.'
/>
);
mfaButtonText = (
<FormattedMessage
id='user.settings.mfa.reset'
defaultMessage='Reset MFA on your account'
/>
);
} else {
mfaRemoveHelp = (
<FormattedMessage
id='user.settings.mfa.removeHelp'
defaultMessage='Removing multi-factor authentication means you will no longer require a phone-based passcode to sign-in to your account.'
/>
);
mfaButtonText = (
<FormattedMessage
id='user.settings.mfa.remove'
defaultMessage='Remove MFA from your account'
/>
);
}
content = ( content = (
<div key='mfaQrCode'> <div key='mfaQrCode'>
<a <a
@@ -219,10 +223,7 @@ export default class SecurityTab extends React.Component {
href='#' href='#'
onClick={this.deactivateMfa} onClick={this.deactivateMfa}
> >
<FormattedMessage {mfaButtonText}
id='user.settings.mfa.remove'
defaultMessage='Remove MFA from your account'
/>
</a> </a>
<br/> <br/>
</div> </div>
@@ -230,78 +231,16 @@ export default class SecurityTab extends React.Component {
extraInfo = ( extraInfo = (
<span> <span>
<FormattedMessage {mfaRemoveHelp}
id='user.settings.mfa.removeHelp'
defaultMessage='Removing multi-factor authentication will make your account more vulnerable to attacks.'
/>
</span> </span>
); );
} else if (this.state.mfaShowQr) {
content = (
<div key='mfaButton'>
<div className='form-group'>
<label className='col-sm-3 control-label'>
<FormattedMessage
id='user.settings.mfa.qrCode'
defaultMessage='Bar Code'
/>
</label>
<div className='col-sm-5'>
<img
className='qr-code-img'
src={'data:image/png;base64,' + this.state.qrCode}
/>
</div>
</div>
<div className='form-group'>
<label className='col-sm-3 control-label'>
<FormattedMessage
id='user.settings.mfa.secret'
defaultMessage='Secret'
/>
</label>
<div className='col-sm-9 padding-top'>
{this.state.secret}
</div>
</div>
<hr/>
<div className='form-group'>
<label className='col-sm-5 control-label'>
<FormattedMessage
id='user.settings.mfa.enterToken'
defaultMessage='Token (numbers only)'
/>
</label>
<div className='col-sm-7'>
<input
className='form-control'
type='number'
autoFocus={true}
onChange={this.updateMfaToken}
value={this.state.mfaToken}
/>
</div>
</div>
</div>
);
extraInfo = (
<span>
<FormattedMessage
id='user.settings.mfa.addHelpQr'
defaultMessage='Please scan the QR code with the Google Authenticator app on your smartphone and fill in the token with one provided by the app. If you are unable to scan the code, you can manually enter the secret provided.'
/>
</span>
);
submit = this.activateMfa;
} else { } else {
content = ( content = (
<div key='mfaQrCode'> <div key='mfaQrCode'>
<a <a
className='btn btn-primary' className='btn btn-primary'
href='#' href='#'
onClick={this.showQrCode} onClick={this.setupMfa}
> >
<FormattedMessage <FormattedMessage
id='user.settings.mfa.add' id='user.settings.mfa.add'
@@ -314,9 +253,9 @@ export default class SecurityTab extends React.Component {
extraInfo = ( extraInfo = (
<span> <span>
<FormattedHTMLMessage <FormattedMessage
id='user.settings.mfa.addHelp' id='user.settings.mfa.addHelp'
defaultMessage="You can require a smartphone-based token, in addition to your password, to sign into Mattermost.<br/><br/>To enable, download Google Authenticator from <a target='_blank' href='https://itunes.apple.com/us/app/google-authenticator/id388497605?mt=8'>iTunes</a> or <a target='_blank' href='https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en'>Google Play</a> for your phone, then<br/><br/>1. Click the <strong>Add MFA to your account</strong> button above.<br/>2. Use Google Authenticator to scan the QR code that appears or type in the secret manually.<br/>3. Type in the Token generated by Google Authenticator and click <strong>Save</strong>.<br/><br/>When logging in, you will be asked to enter a token from Google Authenticator in addition to your regular credentials." defaultMessage='Adding multi-factor authentication will make your account more secure by requiring a code from your mobile phone each time you sign in.'
/> />
</span> </span>
); );
@@ -334,7 +273,7 @@ export default class SecurityTab extends React.Component {
updateSectionStatus = function resetSection(e) { updateSectionStatus = function resetSection(e) {
this.props.updateSection(''); this.props.updateSection('');
this.setState({mfaToken: '', mfaShowQr: false, mfaError: null, serverError: null}); this.setState({serverError: null});
e.preventDefault(); e.preventDefault();
}.bind(this); }.bind(this);
@@ -345,7 +284,6 @@ export default class SecurityTab extends React.Component {
extraInfo={extraInfo} extraInfo={extraInfo}
submit={submit} submit={submit}
server_error={this.state.serverError} server_error={this.state.serverError}
client_error={this.state.mfaError}
updateSection={updateSectionStatus} updateSection={updateSectionStatus}
width='medium' width='medium'
/> />

Просмотреть файл

@@ -136,6 +136,12 @@
"add_outgoing_webhook.triggerWordsTriggerWhen.help": "Choose when to trigger the outgoing webhook; if the first word of a message matches a Trigger Word exactly, or if it starts with a Trigger Word.", "add_outgoing_webhook.triggerWordsTriggerWhen.help": "Choose when to trigger the outgoing webhook; if the first word of a message matches a Trigger Word exactly, or if it starts with a Trigger Word.",
"add_outgoing_webhook.triggerWordsTriggerWhenFullWord": "First word matches a trigger word exactly", "add_outgoing_webhook.triggerWordsTriggerWhenFullWord": "First word matches a trigger word exactly",
"add_outgoing_webhook.triggerWordsTriggerWhenStartsWith": "First word starts with a trigger word", "add_outgoing_webhook.triggerWordsTriggerWhenStartsWith": "First word starts with a trigger word",
"admin.mfa.title": "Multi-factor Authentication",
"admin.mfa.bannerDesc": "Multi-factor authentication is only available for accounts with LDAP and email login methods. If there are users on your system with other login methods, it is recommended you set up multi-factor authentication directly with the SSO or SAML provider.",
"admin.mfa.cluster": "High",
"admin.mfa.cluster": "High",
"admin.mfa.cluster": "High",
"admin.mfa.cluster": "High",
"admin.advance.cluster": "High Availability (Beta)", "admin.advance.cluster": "High Availability (Beta)",
"admin.advance.metrics": "Performance Monitoring (Beta)", "admin.advance.metrics": "Performance Monitoring (Beta)",
"admin.audits.reload": "Reload User Activity Logs", "admin.audits.reload": "Reload User Activity Logs",
@@ -668,6 +674,8 @@
"admin.service.listenExample": "E.g.: \":8065\"", "admin.service.listenExample": "E.g.: \":8065\"",
"admin.service.mfaDesc": "When true, users will be given the option to add multi-factor authentication to their account. They will need a smartphone and an authenticator app such as Google Authenticator.", "admin.service.mfaDesc": "When true, users will be given the option to add multi-factor authentication to their account. They will need a smartphone and an authenticator app such as Google Authenticator.",
"admin.service.mfaTitle": "Enable Multi-factor Authentication:", "admin.service.mfaTitle": "Enable Multi-factor Authentication:",
"admin.service.enforcMfaTitle": "Enforce Multi-factor Authentication:",
"admin.service.enforceMfaDesc": "When true, users on the system will be required to set up [multi-factor authentication]. Any logged in users will be redirected to the multi-factor authentication setup page until they successfully add MFA to their account.<br/><br/>It is recommended you turn on enforcement during non-peak hours, when people are less likely to be using the system. New users will be required to set up multi-factor authentication when they first sign up. After set up, users will not be able to remove multi-factor authentication unless enforcement is disabled.<br/><br/>Please note that multi-factor authentication is only available for accounts with LDAP and email login methods. Mattermost will not enforce multi-factor authentication for other login methods. If there are users on your system using other login methods, it is recommended you set up and enforce multi-factor authentication directly with the SSO or SAML provider.",
"admin.service.mobileSessionDays": "Session length mobile (days):", "admin.service.mobileSessionDays": "Session length mobile (days):",
"admin.service.mobileSessionDaysDesc": "The number of days from the last time a user entered their credentials to the expiry of the user's session. After changing this setting, the new session length will take effect after the next time the user enters their credentials.", "admin.service.mobileSessionDaysDesc": "The number of days from the last time a user entered their credentials to the expiry of the user's session. After changing this setting, the new session length will take effect after the next time the user enters their credentials.",
"admin.service.outWebhooksDesc": "When true, outgoing webhooks will be allowed. See <a href='http://docs.mattermost.com/developer/webhooks-outgoing.html' target='_blank'>documentation</a> to learn more.", "admin.service.outWebhooksDesc": "When true, outgoing webhooks will be allowed. See <a href='http://docs.mattermost.com/developer/webhooks-outgoing.html' target='_blank'>documentation</a> to learn more.",
@@ -1979,14 +1987,29 @@
"user.settings.languages.change": "Change interface language", "user.settings.languages.change": "Change interface language",
"user.settings.languages.promote": "Select which language Mattermost displays in the user interface.<br /><br />Would like to help with translations? Join the <a href='http://translate.mattermost.com/' target='_blank'>Mattermost Translation Server</a> to contribute.", "user.settings.languages.promote": "Select which language Mattermost displays in the user interface.<br /><br />Would like to help with translations? Join the <a href='http://translate.mattermost.com/' target='_blank'>Mattermost Translation Server</a> to contribute.",
"user.settings.mfa.add": "Add MFA to your account", "user.settings.mfa.add": "Add MFA to your account",
"user.settings.mfa.addHelp": "You can require a smartphone-based token, in addition to your password, to sign into Mattermost.<br/><br/>To enable, download Google Authenticator from <a target='_blank' href='https://itunes.apple.com/us/app/google-authenticator/id388497605?mt=8'>iTunes</a> or <a target='_blank' href='https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en'>Google Play</a> for your phone, then<br/><br/>1. Click the <strong>Add MFA to your account</strong> button above.<br/>2. Use Google Authenticator to scan the QR code that appears or type in the secret manually.<br/>3. Type in the Token generated by Google Authenticator and click <strong>Save</strong>.<br/><br/>When logging in, you will be asked to enter a token from Google Authenticator in addition to your regular credentials.", "user.settings.mfa.addHelp": "Adding multi-factor authentication will make your account more secure by requiring a code from your mobile phone each time you sign in.",
"user.settings.mfa.addHelpQr": "Please scan the QR code with the Google Authenticator app on your smartphone and fill in the token with one provided by the app. If you are unable to scan the code, you can manually enter the secret provided.", "user.settings.mfa.addHelpQr": "Please scan the QR code with the Google Authenticator app on your smartphone and fill in the token with one provided by the app. If you are unable to scan the code, you can manually enter the secret provided.",
"user.settings.mfa.enterToken": "Token (numbers only)", "user.settings.mfa.enterToken": "Token (numbers only)",
"user.settings.mfa.qrCode": "Bar Code", "user.settings.mfa.qrCode": "Bar Code",
"user.settings.mfa.remove": "Remove MFA from your account", "user.settings.mfa.remove": "Remove MFA from your account",
"user.settings.mfa.reset": "Reset MFA on your account",
"user.settings.mfa.removeHelp": "Removing multi-factor authentication means you will no longer require a phone-based passcode to sign-in to your account.", "user.settings.mfa.removeHelp": "Removing multi-factor authentication means you will no longer require a phone-based passcode to sign-in to your account.",
"user.settings.mfa.requiredHelp": "Multi-factor authentication is required on this server. Resetting is only recommended when you need to switch code generation to a new mobile device. You will be required to set it up again immediately.",
"user.settings.mfa.secret": "Secret", "user.settings.mfa.secret": "Secret",
"user.settings.mfa.title": "Multi-factor Authentication", "user.settings.mfa.title": "Multi-factor Authentication",
"mfa.setupTitle": "Multi-factor Authentication Setup",
"mfa.setup.codeError": "Please enter the code from Google Authenticator.",
"mfa.setup.badCode": "Invalid code. If this issue persists, contact your System Administrator.",
"mfa.setup.required": "<strong>Multi-factor authentication is required on {siteName}.</strong>",
"mfa.setup.step1": "<strong>Step 1: </strong>On your phone, download Google Authenticator from <a target='_blank' href='https://itunes.apple.com/us/app/google-authenticator/id388497605?mt=8'>iTunes</a> or <a target='_blank' href='https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en'>Google Play</a>",
"mfa.setup.step2": "<strong>Step 2: </strong>Use Google Authenticator to scan this QR code, or manually type in the secret key",
"mfa.setup.secret": "Secret: {secret}",
"mfa.setup.step3": "<strong>Step 3: </strong>Enter the code generated by Google Authenticator",
"mfa.setup.code": "MFA Code",
"mfa.setup.save": "Save",
"mfa.confirm.complete": "<strong>Set up complete!</strong>",
"mfa.confirm.secure": "Your account is now secure. Next time you sign in, you will be asked to enter a code from the Google Authenticator app on your phone.",
"mfa.confirm.okay": "Okay",
"user.settings.modal.advanced": "Advanced", "user.settings.modal.advanced": "Advanced",
"user.settings.modal.confirmBtns": "Yes, Discard", "user.settings.modal.confirmBtns": "Yes, Discard",
"user.settings.modal.confirmMsg": "You have unsaved changes, are you sure you want to discard them?", "user.settings.modal.confirmMsg": "You have unsaved changes, are you sure you want to discard them?",

Просмотреть файл

@@ -21,6 +21,7 @@ import ClusterSettings from 'components/admin_console/cluster_settings.jsx';
import MetricsSettings from 'components/admin_console/metrics_settings.jsx'; import MetricsSettings from 'components/admin_console/metrics_settings.jsx';
import SignupSettings from 'components/admin_console/signup_settings.jsx'; import SignupSettings from 'components/admin_console/signup_settings.jsx';
import PasswordSettings from 'components/admin_console/password_settings.jsx'; import PasswordSettings from 'components/admin_console/password_settings.jsx';
import MfaSettings from 'components/admin_console/mfa_settings.jsx';
import PublicLinkSettings from 'components/admin_console/public_link_settings.jsx'; import PublicLinkSettings from 'components/admin_console/public_link_settings.jsx';
import SessionSettings from 'components/admin_console/session_settings.jsx'; import SessionSettings from 'components/admin_console/session_settings.jsx';
import ConnectionSettings from 'components/admin_console/connection_settings.jsx'; import ConnectionSettings from 'components/admin_console/connection_settings.jsx';
@@ -104,6 +105,10 @@ export default (
path='saml' path='saml'
component={SamlSettings} component={SamlSettings}
/> />
<Route
path='mfa'
component={MfaSettings}
/>
</Route> </Route>
<Route path='security'> <Route path='security'>
<IndexRedirect to='sign_up'/> <IndexRedirect to='sign_up'/>

24
webapp/routes/route_mfa.jsx Обычный файл
Просмотреть файл

@@ -0,0 +1,24 @@
import * as RouteUtils from 'routes/route_utils.jsx';
export default {
path: 'mfa',
getComponents: (location, callback) => {
System.import('components/mfa/mfa_controller.jsx').then(RouteUtils.importComponentSuccess(callback));
},
getChildRoutes: RouteUtils.createGetChildComponentsFunction(
[
{
path: 'setup',
getComponents: (location, callback) => {
System.import('components/mfa/components/setup.jsx').then(RouteUtils.importComponentSuccess(callback));
}
},
{
path: 'confirm',
getComponents: (location, callback) => {
System.import('components/mfa/components/confirm.jsx').then(RouteUtils.importComponentSuccess(callback));
}
}
]
)
};

Просмотреть файл

@@ -6,14 +6,18 @@ import * as RouteUtils from 'routes/route_utils.jsx';
import Root from 'components/root.jsx'; import Root from 'components/root.jsx';
import claimAccountRoute from 'routes/route_claim.jsx'; import claimAccountRoute from 'routes/route_claim.jsx';
import mfaRoute from 'routes/route_mfa.jsx';
import createTeamRoute from 'routes/route_create_team.jsx'; import createTeamRoute from 'routes/route_create_team.jsx';
import teamRoute from 'routes/route_team.jsx'; import teamRoute from 'routes/route_team.jsx';
import helpRoute from 'routes/route_help.jsx'; import helpRoute from 'routes/route_help.jsx';
import BrowserStore from 'stores/browser_store.jsx'; import BrowserStore from 'stores/browser_store.jsx';
import ErrorStore from 'stores/error_store.jsx'; import ErrorStore from 'stores/error_store.jsx';
import UserStore from 'stores/user_store.jsx';
import * as UserAgent from 'utils/user_agent.jsx'; import * as UserAgent from 'utils/user_agent.jsx';
import {browserHistory} from 'react-router/es6';
function preLogin(nextState, replace, callback) { function preLogin(nextState, replace, callback) {
// redirect to the mobile landing page if the user hasn't seen it before // redirect to the mobile landing page if the user hasn't seen it before
if (window.mm_config.IosAppDownloadLink && UserAgent.isIosWeb() && !BrowserStore.hasSeenLandingPage()) { if (window.mm_config.IosAppDownloadLink && UserAgent.isIosWeb() && !BrowserStore.hasSeenLandingPage()) {
@@ -27,7 +31,30 @@ function preLogin(nextState, replace, callback) {
callback(); callback();
} }
const mfaPaths = [
'/mfa/setup',
'/mfa/confirm'
];
const mfaAuthServices = [
'',
'email',
'ldap'
];
function preLoggedIn(nextState, replace, callback) { function preLoggedIn(nextState, replace, callback) {
if (window.mm_license.MFA === 'true' &&
window.mm_config.EnableMultifactorAuthentication === 'true' &&
window.mm_config.EnforceMultifactorAuthentication === 'true' &&
mfaPaths.indexOf(nextState.location.pathname) === -1) {
const user = UserStore.getCurrentUser();
if (user && !user.mfa_active &&
mfaAuthServices.indexOf(user.auth_service) !== -1) {
browserHistory.push('/mfa/setup');
return;
}
}
ErrorStore.clearLastError(); ErrorStore.clearLastError();
callback(); callback();
} }
@@ -154,7 +181,8 @@ export default {
] ]
) )
}, },
teamRoute teamRoute,
mfaRoute
] ]
) )
}, },

Просмотреть файл

@@ -32,6 +32,10 @@ body {
.inner-wrap { .inner-wrap {
height: 100%; height: 100%;
&.sticky {
overflow: auto;
}
> .row { > .row {
&.main { &.main {
height: 100%; height: 100%;

Просмотреть файл

@@ -306,6 +306,7 @@ describe('Client.User', function() {
TestHelper.basicClient().emailToOAuth( TestHelper.basicClient().emailToOAuth(
user.email, user.email,
'new_password', 'new_password',
'',
'gitlab', 'gitlab',
function() { function() {
throw Error('shouldnt work'); throw Error('shouldnt work');
@@ -345,6 +346,7 @@ describe('Client.User', function() {
TestHelper.basicClient().emailToLdap( TestHelper.basicClient().emailToLdap(
user.email, user.email,
user.password, user.password,
'',
'unknown_id', 'unknown_id',
'unknown_pwd', 'unknown_pwd',
function() { function() {
@@ -365,6 +367,7 @@ describe('Client.User', function() {
TestHelper.basicClient().ldapToEmail( TestHelper.basicClient().ldapToEmail(
user.email, user.email,
'new_password', 'new_password',
'',
'new_password', 'new_password',
function() { function() {
throw Error('shouldnt work'); throw Error('shouldnt work');