diff --git a/api4/user.go b/api4/user.go index 0aabc9ef51..45d088c1f7 100644 --- a/api4/user.go +++ b/api4/user.go @@ -63,6 +63,7 @@ func (api *API) InitUser() { api.BaseRoutes.Users.Handle("/login", api.ApiHandler(login)).Methods("POST") api.BaseRoutes.Users.Handle("/login/switch", api.ApiHandler(switchAccountType)).Methods("POST") + api.BaseRoutes.Users.Handle("/login/cws", api.ApiHandlerTrustRequester(loginCWS)).Methods("POST") api.BaseRoutes.Users.Handle("/logout", api.ApiHandler(logout)).Methods("POST") api.BaseRoutes.UserByUsername.Handle("", api.ApiSessionRequired(getUserByUsername)).Methods("GET") @@ -1652,7 +1653,6 @@ func login(c *Context, w http.ResponseWriter, r *http.Request) { }() props := model.MapFromJson(r.Body) - id := props["id"] loginId := props["login_id"] password := props["password"] @@ -1686,7 +1686,7 @@ func login(c *Context, w http.ResponseWriter, r *http.Request) { c.LogAuditWithUserId(id, "attempt - login_id="+loginId) - user, err := c.App.AuthenticateUserForLogin(id, loginId, password, mfaToken, ldapOnly) + user, err := c.App.AuthenticateUserForLogin(id, loginId, password, mfaToken, "", ldapOnly) if err != nil { c.LogAuditWithUserId(id, "failure - login_id="+loginId) c.Err = err @@ -1736,6 +1736,48 @@ func login(c *Context, w http.ResponseWriter, r *http.Request) { w.Write([]byte(user.ToJson())) } +func loginCWS(c *Context, w http.ResponseWriter, r *http.Request) { + if c.App.Srv().License() == nil || !*c.App.Srv().License().Features.Cloud { + c.Err = model.NewAppError("loginCWS", "api.user.login_cws.license.error", nil, "", http.StatusUnauthorized) + return + } + r.ParseForm() + var loginID string + var token string + if len(r.Form) > 0 { + for key, value := range r.Form { + if key == "login_id" { + loginID = value[0] + } + if key == "cws_token" { + token = value[0] + } + } + } + + auditRec := c.MakeAuditRecord("login", audit.Fail) + defer c.LogAuditRec(auditRec) + auditRec.AddMeta("login_id", loginID) + user, err := c.App.AuthenticateUserForLogin("", loginID, "", "", token, false) + if err != nil { + c.LogAuditWithUserId("", "failure - login_id="+loginID) + mlog.Error("CWS authentication error", mlog.Err(err)) + http.Redirect(w, r, *c.App.Config().ServiceSettings.SiteURL, 302) + return + } + auditRec.AddMeta("user", user) + c.LogAuditWithUserId(user.Id, "authenticated") + err = c.App.DoLogin(w, r, user, "", false, false, false) + if err != nil { + mlog.Error("CWS login error", mlog.Err(err)) + http.Redirect(w, r, *c.App.Config().ServiceSettings.SiteURL, 302) + return + } + c.LogAuditWithUserId(user.Id, "success") + c.App.AttachSessionCookies(w, r) + http.Redirect(w, r, *c.App.Config().ServiceSettings.SiteURL, 302) +} + func logout(c *Context, w http.ResponseWriter, r *http.Request) { Logout(c, w, r) } diff --git a/app/app_iface.go b/app/app_iface.go index 2888bc1a1f..d66feaf7fe 100644 --- a/app/app_iface.go +++ b/app/app_iface.go @@ -361,7 +361,7 @@ type AppIface interface { AsymmetricSigningKey() *ecdsa.PrivateKey AttachDeviceId(sessionId string, deviceId string, expiresAt int64) *model.AppError AttachSessionCookies(w http.ResponseWriter, r *http.Request) - AuthenticateUserForLogin(id, loginId, password, mfaToken string, ldapOnly bool) (user *model.User, err *model.AppError) + AuthenticateUserForLogin(id, loginId, password, mfaToken, cwsToken string, ldapOnly bool) (user *model.User, err *model.AppError) AuthorizeOAuthUser(w http.ResponseWriter, r *http.Request, service, code, state, redirectUri string) (io.ReadCloser, string, map[string]string, *model.AppError) AutocompleteChannels(teamId string, term string) (*model.ChannelList, *model.AppError) AutocompleteChannelsForSearch(teamId string, userId string, term string) (*model.ChannelList, *model.AppError) diff --git a/app/login.go b/app/login.go index 4eaba3252a..33e20f8c75 100644 --- a/app/login.go +++ b/app/login.go @@ -4,19 +4,25 @@ package app import ( + "crypto/subtle" + "errors" "fmt" "net/http" + "os" "strconv" "strings" "time" "github.com/avct/uasurfer" + "github.com/mattermost/mattermost-server/v5/mlog" "github.com/mattermost/mattermost-server/v5/model" "github.com/mattermost/mattermost-server/v5/plugin" "github.com/mattermost/mattermost-server/v5/store" "github.com/mattermost/mattermost-server/v5/utils" ) +const cwsTokenEnv = "CWS_CLOUD_TOKEN" + func (a *App) CheckForClientSideCert(r *http.Request) (string, string, string) { pem := r.Header.Get("X-SSL-Client-Cert") // mapped to $ssl_client_cert from nginx subject := r.Header.Get("X-SSL-Client-Cert-Subject-DN") // mapped to $ssl_client_s_dn from nginx @@ -34,7 +40,7 @@ func (a *App) CheckForClientSideCert(r *http.Request) (string, string, string) { return pem, subject, email } -func (a *App) AuthenticateUserForLogin(id, loginId, password, mfaToken string, ldapOnly bool) (user *model.User, err *model.AppError) { +func (a *App) AuthenticateUserForLogin(id, loginId, password, mfaToken, cwsToken string, ldapOnly bool) (user *model.User, err *model.AppError) { // Do statistics defer func() { if a.Metrics() != nil { @@ -46,7 +52,7 @@ func (a *App) AuthenticateUserForLogin(id, loginId, password, mfaToken string, l } }() - if len(password) == 0 { + if len(password) == 0 && !IsCWSLogin(a, cwsToken) { return nil, model.NewAppError("AuthenticateUserForLogin", "api.user.login.blank_pwd.app_error", nil, "", http.StatusBadRequest) } @@ -55,6 +61,42 @@ func (a *App) AuthenticateUserForLogin(id, loginId, password, mfaToken string, l return nil, err } + // CWS login allow to use the one-time token to login the users when they're redirected to their + // installation for the first time + if IsCWSLogin(a, cwsToken) { + if err = checkUserNotBot(user); err != nil { + return nil, err + } + token, err := a.Srv().Store.Token().GetByToken(cwsToken) + if nfErr := new(store.ErrNotFound); err != nil && !errors.As(err, &nfErr) { + mlog.Error("error retrieving the cws token from the store", mlog.Err(err)) + return nil, model.NewAppError("AuthenticateUserForLogin", + "api.user.login_by_cws.invalid_token.app_error", nil, "", http.StatusInternalServerError) + } + // If token is stored in the database that means it was used + if token != nil { + return nil, model.NewAppError("AuthenticateUserForLogin", + "api.user.login_by_cws.invalid_token.app_error", nil, "", http.StatusBadRequest) + } + envToken, ok := os.LookupEnv(cwsTokenEnv) + if ok && subtle.ConstantTimeCompare([]byte(envToken), []byte(cwsToken)) == 1 { + token = &model.Token{ + Token: cwsToken, + CreateAt: model.GetMillis(), + Type: TOKEN_TYPE_CWS_ACCESS, + } + err := a.Srv().Store.Token().Save(token) + if err != nil { + mlog.Error("error storing the cws token in the store", mlog.Err(err)) + return nil, model.NewAppError("AuthenticateUserForLogin", + "api.user.login_by_cws.invalid_token.app_error", nil, "", http.StatusInternalServerError) + } + return user, nil + } + return nil, model.NewAppError("AuthenticateUserForLogin", + "api.user.login_by_cws.invalid_token.app_error", nil, "", http.StatusBadRequest) + } + // If client side cert is enable and it's checking as a primary source // then trust the proxy and cert that the correct user is supplied and allow // them access @@ -246,3 +288,7 @@ func GetProtocol(r *http.Request) string { } return "http" } + +func IsCWSLogin(a *App, token string) bool { + return a.Srv().License() != nil && *a.Srv().License().Features.Cloud && token != "" +} diff --git a/app/login_test.go b/app/login_test.go index 23f7a55707..68c18bc57f 100644 --- a/app/login_test.go +++ b/app/login_test.go @@ -5,8 +5,10 @@ package app import ( "net/http" + "os" "testing" + "github.com/mattermost/mattermost-server/v5/model" "github.com/stretchr/testify/require" ) @@ -35,3 +37,35 @@ func TestCheckForClientSideCert(t *testing.T) { require.Equal(t, actualEmail, tt.expectedEmail, "CheckForClientSideCert(%v): expected %v, actual %v", tt.subject, tt.expectedEmail, actualEmail) } } + +func TestCWSLogin(t *testing.T) { + + th := Setup(t).InitBasic() + defer th.TearDown() + license := model.NewTestLicense() + license.Features.Cloud = model.NewBool(true) + th.App.Srv().SetLicense(license) + + t.Run("Should authenticate user when CWS login is enabled and tokens are equal", func(t *testing.T) { + token := model.NewToken(TOKEN_TYPE_CWS_ACCESS, "") + defer th.App.DeleteToken(token) + os.Setenv("CWS_CLOUD_TOKEN", token.Token) + user, err := th.App.AuthenticateUserForLogin("", th.BasicUser.Username, "", "", token.Token, false) + require.Nil(t, err) + require.NotNil(t, user) + require.Equal(t, th.BasicUser.Username, user.Username) + _, apperr := th.App.Srv().Store.Token().GetByToken(token.Token) + require.Nil(t, apperr) + th.App.DeleteToken(token) + }) + + t.Run("Should not authenticate the user when CWS token was used", func(t *testing.T) { + token := model.NewToken(TOKEN_TYPE_CWS_ACCESS, "") + os.Setenv("CWS_CLOUD_TOKEN", token.Token) + require.Nil(t, th.App.Srv().Store.Token().Save(token)) + defer th.App.DeleteToken(token) + user, err := th.App.AuthenticateUserForLogin("", th.BasicUser.Username, "", "", token.Token, false) + require.Error(t, err) + require.Nil(t, user) + }) +} diff --git a/app/opentracing/opentracing_layer.go b/app/opentracing/opentracing_layer.go index 5bb445bfce..62d04ab534 100644 --- a/app/opentracing/opentracing_layer.go +++ b/app/opentracing/opentracing_layer.go @@ -599,7 +599,7 @@ func (a *OpenTracingAppLayer) AttachSessionCookies(w http.ResponseWriter, r *htt a.app.AttachSessionCookies(w, r) } -func (a *OpenTracingAppLayer) AuthenticateUserForLogin(id string, loginId string, password string, mfaToken string, ldapOnly bool) (user *model.User, err *model.AppError) { +func (a *OpenTracingAppLayer) AuthenticateUserForLogin(id string, loginId string, password string, mfaToken string, cwsToken string, ldapOnly bool) (user *model.User, err *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.AuthenticateUserForLogin") @@ -611,7 +611,7 @@ func (a *OpenTracingAppLayer) AuthenticateUserForLogin(id string, loginId string }() defer span.Finish() - resultVar0, resultVar1 := a.app.AuthenticateUserForLogin(id, loginId, password, mfaToken, ldapOnly) + resultVar0, resultVar1 := a.app.AuthenticateUserForLogin(id, loginId, password, mfaToken, cwsToken, ldapOnly) if resultVar1 != nil { span.LogFields(spanlog.Error(resultVar1)) diff --git a/app/user.go b/app/user.go index 527088b89c..3e6cee7d7c 100644 --- a/app/user.go +++ b/app/user.go @@ -42,6 +42,7 @@ const ( TOKEN_TYPE_VERIFY_EMAIL = "verify_email" TOKEN_TYPE_TEAM_INVITATION = "team_invitation" TOKEN_TYPE_GUEST_INVITATION = "guest_invitation" + TOKEN_TYPE_CWS_ACCESS = "cws_access_token" PASSWORD_RECOVER_EXPIRY_TIME = 1000 * 60 * 60 // 1 hour INVITATION_EXPIRY_TIME = 1000 * 60 * 60 * 48 // 48 hours IMAGE_PROFILE_PIXEL_DIMENSION = 128 diff --git a/i18n/en.json b/i18n/en.json index 97cf69d52f..5746d4757a 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -2906,6 +2906,10 @@ "id": "api.user.login.use_auth_service.app_error", "translation": "Please sign in using {{.AuthService}}." }, + { + "id": "api.user.login_by_cws.invalid_token.app_error", + "translation": "CWS token is not valid" + }, { "id": "api.user.login_by_oauth.bot_login_forbidden.app_error", "translation": "Bot login is forbidden." @@ -2918,6 +2922,10 @@ "id": "api.user.login_by_oauth.parse.app_error", "translation": "Could not parse auth data out of {{.Service}} user object." }, + { + "id": "api.user.login_cws.license.error", + "translation": "CWS login is forbidden." + }, { "id": "api.user.login_ldap.not_available.app_error", "translation": "AD/LDAP not available on this server."