[MM-28083] CWS one-time login logic (#15356)

* Cloud token login

This PR adds the capability of activate the cloud token login that
will be used in our Cloud installations to let the customer login
for the first time without using credentials.

* Read CSRF from cookie when is not on the header and we're login with CWS

* Create new CWS login endpoint

- New endpoint created
- We're using the cloud feature from the license instead of the
configuration flag
- Removed the CSRF changes

* Reduce amount of work if cws token is not set

* Removed unused config key

* Now we store the token to detect it was used

If the token is in the token store then we are assuming that the
token was used

* Add tests

* Add i18n strings
Этот коммит содержится в:
Mario de Frutos Dieguez
2020-09-01 14:50:43 +02:00
коммит произвёл GitHub
родитель 26cdbd5dba
Коммит 22297a9bf4
7 изменённых файлов: 138 добавлений и 7 удалений

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

@@ -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)
}

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

@@ -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)

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

@@ -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 != ""
}

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

@@ -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)
})
}

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

@@ -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))

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

@@ -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

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

@@ -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."