More work on authentication

This commit is contained in:
Jesús Espino 2020-11-06 16:46:35 +01:00
parent 85c8a5a966
commit 35ebd44d24
11 changed files with 261 additions and 9 deletions

View file

@ -8,5 +8,6 @@
"webpath": "./webapp/pack",
"filespath": "./files",
"telemetry": true,
"webhook_update": []
"webhook_update": [],
"secret": "this-is-a-secret-string"
}

View file

@ -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("{}")

104
server/api/auth.go Normal file
View file

@ -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, &registerData)
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
}

83
server/app/auth.go Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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