More work on authentication
This commit is contained in:
parent
85c8a5a966
commit
35ebd44d24
11 changed files with 261 additions and 9 deletions
|
@ -8,5 +8,6 @@
|
|||
"webpath": "./webapp/pack",
|
||||
"filespath": "./files",
|
||||
"telemetry": true,
|
||||
"webhook_update": []
|
||||
"webhook_update": [],
|
||||
"secret": "this-is-a-secret-string"
|
||||
}
|
||||
|
|
|
@ -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
104
server/api/auth.go
Normal 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, ®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
|
||||
}
|
83
server/app/auth.go
Normal file
83
server/app/auth.go
Normal 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
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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=
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
20
server/services/auth/token.go
Normal file
20
server/services/auth/token.go
Normal 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
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue