diff --git a/config.json b/config.json index c15fb9379..ab3d8b48a 100644 --- a/config.json +++ b/config.json @@ -8,5 +8,6 @@ "webpath": "./webapp/pack", "filespath": "./files", "telemetry": true, - "webhook_update": [] + "webhook_update": [], + "secret": "this-is-a-secret-string" } diff --git a/server/api/api.go b/server/api/api.go index a33e61837..e80ae55db 100644 --- a/server/api/api.go +++ b/server/api/api.go @@ -36,6 +36,9 @@ func (a *API) RegisterRoutes(r *mux.Router) { r.HandleFunc("/api/v1/blocks/{blockID}", a.handleDeleteBlock).Methods("DELETE") r.HandleFunc("/api/v1/blocks/{blockID}/subtree", a.handleGetSubTree).Methods("GET") + r.HandleFunc("/api/v1/login", a.handleLogin).Methods("POST") + r.HandleFunc("/api/v1/register", a.handleRegister).Methods("POST") + r.HandleFunc("/api/v1/files", a.handleUploadFile).Methods("POST") r.HandleFunc("/files/{filename}", a.handleServeFile).Methods("GET") @@ -293,6 +296,7 @@ func jsonBytesResponse(w http.ResponseWriter, code int, json []byte) { func errorResponse(w http.ResponseWriter, code int, message map[string]string) { log.Printf("%d ERROR", code) + log.Printf("%v ERROR", message) data, err := json.Marshal(message) if err != nil { data = []byte("{}") diff --git a/server/api/auth.go b/server/api/auth.go new file mode 100644 index 000000000..0f99b099f --- /dev/null +++ b/server/api/auth.go @@ -0,0 +1,104 @@ +package api + +import ( + "encoding/json" + "errors" + "io/ioutil" + "log" + "net/http" + "strings" +) + +type LoginData struct { + Type string `json:"type"` + Username string `json:"username"` + Email string `json:"email"` + Password string `json:"password"` + MfaToken string `json:"mfa_token"` +} + +type RegisterData struct { + Username string `json:"username"` + Email string `json:"email"` + Password string `json:"password"` +} + +func (rd *RegisterData) IsValid() error { + if rd.Username == "" { + return errors.New("Username is required") + } + if rd.Email == "" { + return errors.New("Email is required") + } + if !strings.Contains(rd.Email, "@") { + return errors.New("Invalid email format") + } + if !strings.Contains(rd.Password, "") { + return errors.New("Password is required") + } + return nil +} + +func (a *API) handleLogin(w http.ResponseWriter, r *http.Request) { + requestBody, err := ioutil.ReadAll(r.Body) + if err != nil { + errorResponse(w, http.StatusInternalServerError, nil) + + return + } + + var loginData LoginData + err = json.Unmarshal(requestBody, &loginData) + if err != nil { + errorResponse(w, http.StatusInternalServerError, nil) + return + } + + if loginData.Type == "normal" { + jwtToken, err := a.app().Login(loginData.Username, loginData.Email, loginData.Password, loginData.MfaToken) + if err != nil { + errorResponse(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) + return + } + json, err := json.Marshal(jwtToken) + if err != nil { + log.Printf(`ERROR json.Marshal: %v`, r) + errorResponse(w, http.StatusInternalServerError, nil) + return + } + + jsonBytesResponse(w, http.StatusOK, json) + } + + errorResponse(w, http.StatusInternalServerError, map[string]string{"error": "Unknown login type"}) + return +} + +func (a *API) handleRegister(w http.ResponseWriter, r *http.Request) { + requestBody, err := ioutil.ReadAll(r.Body) + if err != nil { + errorResponse(w, http.StatusInternalServerError, nil) + + return + } + + var registerData RegisterData + err = json.Unmarshal(requestBody, ®isterData) + if err != nil { + errorResponse(w, http.StatusInternalServerError, nil) + return + } + + if err = registerData.IsValid(); err != nil { + errorResponse(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) + return + } + + err = a.app().RegisterUser(registerData.Username, registerData.Email, registerData.Password) + if err != nil { + errorResponse(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) + return + } + jsonBytesResponse(w, http.StatusOK, nil) + return +} diff --git a/server/app/auth.go b/server/app/auth.go new file mode 100644 index 000000000..a12466747 --- /dev/null +++ b/server/app/auth.go @@ -0,0 +1,83 @@ +package app + +import ( + "github.com/google/uuid" + "github.com/mattermost/mattermost-octo-tasks/server/model" + "github.com/mattermost/mattermost-octo-tasks/server/services/auth" + + "github.com/pkg/errors" +) + +func (a *App) Login(username string, email string, password string, mfaToken string) (string, error) { + var user *model.User + if username != "" { + var err error + user, err = a.store.GetUserByUsername(username) + if err != nil { + return "", errors.Wrap(err, "invalid username or password") + } + } + + if user == nil && email != "" { + var err error + user, err = a.store.GetUserByEmail(email) + if err != nil { + return "", errors.Wrap(err, "invalid username or password") + } + } + if user == nil { + return "", errors.New("invalid username or password") + } + + if !auth.ComparePassword(user.Password, password) { + return "", errors.New("invalid username or password") + } + + // TODO: MFA verification + return auth.CreateToken(user.ID, a.config.Secret) +} + +func (a *App) RegisterUser(username string, email string, password string) error { + var user *model.User + if username != "" { + var err error + user, err = a.store.GetUserByUsername(username) + if err == nil && user != nil { + return errors.Wrap(err, "The username already exists") + } + } + + if user == nil && email != "" { + var err error + user, err = a.store.GetUserByEmail(email) + if err == nil && user != nil { + return errors.Wrap(err, "The email already exists") + } + } + + // TODO: Move this into the config + passwordSettings := auth.PasswordSettings{ + MinimumLength: 6, + } + + err := auth.IsPasswordValid(password, passwordSettings) + if err != nil { + return errors.Wrap(err, "Invalid password") + } + + err = a.store.CreateUser(&model.User{ + ID: uuid.New().String(), + Username: username, + Email: email, + Password: auth.HashPassword(password), + MfaSecret: "", + AuthService: "", + AuthData: "", + Props: map[string]interface{}{}, + }) + if err != nil { + return errors.Wrap(err, "Unable to create the new user") + } + + return nil +} diff --git a/server/go.mod b/server/go.mod index 01bce9264..60b864b45 100644 --- a/server/go.mod +++ b/server/go.mod @@ -4,6 +4,7 @@ go 1.15 require ( github.com/Masterminds/squirrel v1.4.0 + github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/go-ldap/ldap v3.0.3+incompatible // indirect github.com/golang-migrate/migrate v3.5.4+incompatible github.com/golang-migrate/migrate/v4 v4.13.0 @@ -20,10 +21,12 @@ require ( github.com/mattn/go-sqlite3 v2.0.3+incompatible github.com/nicksnyder/go-i18n v1.10.1 // indirect github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect + github.com/pkg/errors v0.9.1 github.com/rudderlabs/analytics-go v3.2.1+incompatible github.com/spf13/viper v1.7.1 github.com/stretchr/testify v1.6.1 go.uber.org/zap v1.15.0 + golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de golang.org/x/tools v0.0.0-20201017001424-6003fad69a88 // indirect gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d // indirect gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect diff --git a/server/go.sum b/server/go.sum index 658403bf3..e734aa074 100644 --- a/server/go.sum +++ b/server/go.sum @@ -186,6 +186,7 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/denisenkom/go-mssqldb v0.0.0-20200620013148-b91950f658ec/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/dgoogauth v0.0.0-20190221195224-5a805980a5f3 h1:AqeKSZIG/NIC75MNQlPy/LM3LxfpLwahICJBHwSMFNc= github.com/dgryski/dgoogauth v0.0.0-20190221195224-5a805980a5f3/go.mod h1:hEfFauPHz7+NnjR/yHJGhrKo1Za+zStgwUETx3yzqgY= diff --git a/server/services/auth/password.go b/server/services/auth/password.go index e5925ad06..b9b31ee68 100644 --- a/server/services/auth/password.go +++ b/server/services/auth/password.go @@ -81,12 +81,12 @@ type PasswordSettings struct { Symbol bool } -func (as *AuthService) IsPasswordValid(password string) error { +func IsPasswordValid(password string, settings PasswordSettings) error { err := &InvalidPasswordError{ FailingCriterias: []string{}, } - if len(password) < as.passwordSettings.MinimumLength { + if len(password) < settings.MinimumLength { err.FailingCriterias = append(err.FailingCriterias, InvalidMinLengthPassword) } @@ -94,25 +94,25 @@ func (as *AuthService) IsPasswordValid(password string) error { err.FailingCriterias = append(err.FailingCriterias, InvalidMaxLengthPassword) } - if as.passwordSettings.Lowercase { + if settings.Lowercase { if !strings.ContainsAny(password, PasswordLowerCaseLetters) { err.FailingCriterias = append(err.FailingCriterias, InvalidLowercasePassword) } } - if as.passwordSettings.Uppercase { + if settings.Uppercase { if !strings.ContainsAny(password, PasswordUpperCaseLetters) { err.FailingCriterias = append(err.FailingCriterias, InvalidUppercasePassword) } } - if as.passwordSettings.Number { + if settings.Number { if !strings.ContainsAny(password, PasswordNumbers) { err.FailingCriterias = append(err.FailingCriterias, InvalidNumberPassword) } } - if as.passwordSettings.Symbol { + if settings.Symbol { if !strings.ContainsAny(password, PasswordSpecialChars) { err.FailingCriterias = append(err.FailingCriterias, InvalidSymbolPassword) } diff --git a/server/services/auth/token.go b/server/services/auth/token.go new file mode 100644 index 000000000..c62856936 --- /dev/null +++ b/server/services/auth/token.go @@ -0,0 +1,20 @@ +package auth + +import ( + "time" + + "github.com/dgrijalva/jwt-go" +) + +func CreateToken(userID string, appSecret string) (string, error) { + claims := jwt.MapClaims{} + claims["authorized"] = true + claims["user_id"] = userID + claims["exp"] = time.Now().Add(time.Minute * 15).Unix() + at := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + token, err := at.SignedString([]byte(appSecret)) + if err != nil { + return "", err + } + return token, nil +} diff --git a/server/services/config/config.go b/server/services/config/config.go index b63bf30a2..e33db199b 100644 --- a/server/services/config/config.go +++ b/server/services/config/config.go @@ -22,6 +22,7 @@ type Configuration struct { FilesPath string `json:"filespath" mapstructure:"filespath"` Telemetry bool `json:"telemetry" mapstructure:"telemetry"` WebhookUpdate []string `json:"webhook_update" mapstructure:"webhook_update"` + Secret string `json:"secret" mapstructure:"secret"` } // ReadConfigFile read the configuration from the filesystem. diff --git a/server/services/store/sqlstore/user.go b/server/services/store/sqlstore/user.go index ee380d5e4..5b82e10b4 100644 --- a/server/services/store/sqlstore/user.go +++ b/server/services/store/sqlstore/user.go @@ -1,6 +1,7 @@ package sqlstore import ( + "encoding/json" "time" "github.com/mattermost/mattermost-octo-tasks/server/model" @@ -16,7 +17,13 @@ func (s *SQLStore) getUserByCondition(condition sq.Eq) (*model.User, error) { row := query.QueryRow() user := model.User{} - err := row.Scan(&user.ID, &user.Username, &user.Email, &user.Password, &user.MfaSecret, &user.AuthService, &user.AuthData, &user.Props, &user.CreateAt, &user.UpdateAt, &user.DeleteAt) + var propsBytes []byte + err := row.Scan(&user.ID, &user.Username, &user.Email, &user.Password, &user.MfaSecret, &user.AuthService, &user.AuthData, &propsBytes, &user.CreateAt, &user.UpdateAt, &user.DeleteAt) + if err != nil { + return nil, err + } + + err = json.Unmarshal(propsBytes, &user.Props) if err != nil { return nil, err } @@ -39,10 +46,33 @@ func (s *SQLStore) GetUserByUsername(username string) (*model.User, error) { func (s *SQLStore) CreateUser(user *model.User) error { now := time.Now().Unix() + propsBytes, err := json.Marshal(user.Props) + if err != nil { + return err + } + query := s.getQueryBuilder().Insert("users"). Columns("id", "username", "email", "password", "mfa_secret", "auth_service", "auth_data", "props", "create_at", "update_at", "delete_at"). - Values(user.ID, user.Username, user.Email, user.Password, user.MfaSecret, user.AuthService, user.AuthData, user.Props, now, now, 0) + Values(user.ID, user.Username, user.Email, user.Password, user.MfaSecret, user.AuthService, user.AuthData, propsBytes, now, now, 0) + + _, err = query.Exec() + return err } func (s *SQLStore) UpdateUser(user *model.User) error { + now := time.Now().Unix() + + propsBytes, err := json.Marshal(user.Props) + if err != nil { + return err + } + + query := s.getQueryBuilder().Update("users"). + Set("username", user.Username). + Set("email", user.Email). + Set("props", propsBytes). + Set("update_at", now) + + _, err = query.Exec() + return err } diff --git a/server/services/store/store.go b/server/services/store/store.go index 6d16a5552..00a560319 100644 --- a/server/services/store/store.go +++ b/server/services/store/store.go @@ -16,4 +16,9 @@ type Store interface { Shutdown() error GetSystemSettings() (map[string]string, error) SetSystemSetting(key string, value string) error + GetUserById(userID string) (*model.User, error) + GetUserByEmail(email string) (*model.User, error) + GetUserByUsername(username string) (*model.User, error) + CreateUser(user *model.User) error + UpdateUser(user *model.User) error }