From 262321a1e2c503ac2b16611d1aef8fe6d7dd1088 Mon Sep 17 00:00:00 2001 From: Deepak Date: Tue, 12 Jan 2021 12:43:45 -0600 Subject: [PATCH] Adds auth route that checks username and password --- models/errors.go | 25 +++++++++++ models/errors_test.go | 7 ++++ models/models_test.go | 3 +- models/user.go | 18 +++++--- models/user_test.go | 8 +++- routes/auth.go | 96 +++++++++++++++++++++++++++++++++++++++++++ routes/errors.go | 5 +++ routes/routes.go | 1 + routes/users.go | 36 ---------------- 9 files changed, 155 insertions(+), 44 deletions(-) create mode 100644 routes/auth.go delete mode 100644 routes/users.go diff --git a/models/errors.go b/models/errors.go index 8274dc4..6d071fb 100644 --- a/models/errors.go +++ b/models/errors.go @@ -2,6 +2,7 @@ package models import ( "database/sql" + "golang.org/x/crypto/bcrypt" ) type notFoundError struct { @@ -27,3 +28,27 @@ func wrapNotFound(err error) error { } return err } + +type invalidLoginError struct { + error +} + +func (e *invalidLoginError) InvalidLogin() bool { + return true +} + +// IsInvalidLoginError returns true if the model deems it an invalid login error. +func IsInvalidLoginError(err error) bool { + type invalidLogin interface { + InvalidLogin() bool + } + te, ok := err.(invalidLogin) + return ok && te.InvalidLogin() +} + +func wrapInvalidLogin(err error) error { + if err == sql.ErrNoRows || err == bcrypt.ErrMismatchedHashAndPassword { + return &invalidLoginError{error: err} + } + return err +} diff --git a/models/errors_test.go b/models/errors_test.go index 88baab0..20b0749 100644 --- a/models/errors_test.go +++ b/models/errors_test.go @@ -40,3 +40,10 @@ func TestErrorModelWrapping(t *testing.T) { _, err = m.Action(0) assert.True(models.IsNotFoundError(err)) } +func TestErrorModelInvalidLogin(t *testing.T) { + assert := assert.New(t) + m := getErrorModel(sql.ErrNoRows) + + _, err := m.VerifyUserByUsernamePassword("duck", "duck") + assert.True(models.IsInvalidLoginError(err)) +} diff --git a/models/models_test.go b/models/models_test.go index b9693b2..3c4fc7f 100644 --- a/models/models_test.go +++ b/models/models_test.go @@ -44,7 +44,8 @@ func (ms *multiStore) SelectActionsByPlanID(plan *models.Plan) ([]*models.Action } func (ms *multiStore) SelectUserByUsername(username string) (*models.User, error) { - return &models.User{UserID: int64(1), Username: username, DisplayName: "Ted Est", Password: []byte("oh no")}, nil + // password is "password" + return &models.User{UserID: int64(1), Username: username, DisplayName: "Ted Est", Password: []byte("$2y$05$6SVV35GX4cB4PDPhRaDD/exsL.HV8QtMMr60YL6dLyqtX4l58q.cy")}, nil } func (ms *multiStore) InsertUser(user *models.User) (int, error) { diff --git a/models/user.go b/models/user.go index f19c882..bcadb10 100644 --- a/models/user.go +++ b/models/user.go @@ -22,13 +22,21 @@ type UserNoPassword struct { DisplayName string `json:"display_name"` } -// UserByUsername returns a single user by the unique username. -func (m *Model) UserByUsername(username string) (*UserNoPassword, error) { +// VerifyUserByUsernamePassword returns a single user by the unique username, if the provided password is correct. +func (m *Model) VerifyUserByUsernamePassword(username string, password string) (*UserNoPassword, error) { user, err := m.SelectUserByUsername(username) - if user == nil { - return nil, wrapNotFound(err) + if err != nil { + // throwaway to pad time + hashPassword(username) + return nil, wrapInvalidLogin(err) } - return user.NoPassword(), wrapNotFound(err) + + err = bcrypt.CompareHashAndPassword(user.Password, []byte(password)) + if err != nil { + return nil, wrapInvalidLogin(err) + } + + return user.NoPassword(), nil } // NoPassword strips the user of password. diff --git a/models/user_test.go b/models/user_test.go index be4226e..eb58432 100644 --- a/models/user_test.go +++ b/models/user_test.go @@ -17,16 +17,20 @@ func TestModelUsers(t *testing.T) { []*models.Plan{p}} m := models.New(ss) - user, err := m.UserByUsername("test") + user, err := m.VerifyUserByUsernamePassword("test", "password") assert.Nil(err) assert.NotNil(user) + + user, err = m.VerifyUserByUsernamePassword("test", "wrong_password") + assert.NotNil(err) + assert.Nil(user) } func TestErrorUsers(t *testing.T) { assert := assert.New(t) m := getErrorModel(fmt.Errorf("err")) - user, err := m.UserByUsername("snth") + user, err := m.VerifyUserByUsernamePassword("snth", "aoeu") assert.Nil(user) assert.NotNil(err) } diff --git a/routes/auth.go b/routes/auth.go new file mode 100644 index 0000000..232846a --- /dev/null +++ b/routes/auth.go @@ -0,0 +1,96 @@ +package routes + +import ( + "encoding/json" + "gitea.deepak.science/deepak/gogmagog/models" + "github.com/go-chi/chi" + "io" + "net/http" +) + +func newAuthRouter(m *models.Model) http.Handler { + router := chi.NewRouter() + router.Post("/register", postUserFunc(m)) + router.Post("/tokens", createTokenFunc(m)) + return router +} + +type createUserResponse struct { + Username string `json:"username"` +} + +func postUserFunc(m *models.Model) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + r.Body = http.MaxBytesReader(w, r.Body, 1024) + dec := json.NewDecoder(r.Body) + dec.DisallowUnknownFields() + var req models.CreateUserRequest + err := dec.Decode(&req) + if err != nil { + badRequestError(w, err) + return + } + err = dec.Decode(&struct{}{}) + if err != io.EOF { + badRequestError(w, err) + return + } + + _, err = m.CreateUser(&req) + if err != nil { + serverError(w, err) + return + } + + response := &createUserResponse{ + Username: req.Username, + } + w.WriteHeader(http.StatusCreated) + w.Header().Add("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(response); err != nil { + serverError(w, err) + } + + } +} + +type loginCreds struct { + Username string `json:"username"` + Password string `json:"password"` +} + +func createTokenFunc(m *models.Model) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + r.Body = http.MaxBytesReader(w, r.Body, 1024) + dec := json.NewDecoder(r.Body) + dec.DisallowUnknownFields() + var creds loginCreds + err := dec.Decode(&creds) + if err != nil { + badRequestError(w, err) + return + } + err = dec.Decode(&struct{}{}) + if err != io.EOF { + badRequestError(w, err) + return + } + + user, err := m.VerifyUserByUsernamePassword(creds.Username, creds.Password) + if err != nil { + if models.IsInvalidLoginError(err) { + unauthorizedHandler(w, r) + return + } + serverError(w, err) + return + + } + w.Header().Add("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(user); err != nil { + serverError(w, err) + } + } +} diff --git a/routes/errors.go b/routes/errors.go index a43793a..43a457a 100644 --- a/routes/errors.go +++ b/routes/errors.go @@ -25,3 +25,8 @@ func notFoundHandler(w http.ResponseWriter, r *http.Request) { code := http.StatusNotFound http.Error(w, http.StatusText(code), code) } + +func unauthorizedHandler(w http.ResponseWriter, r *http.Request) { + code := http.StatusUnauthorized + http.Error(w, http.StatusText(code), code) +} diff --git a/routes/routes.go b/routes/routes.go index 57e189e..e1b6c22 100644 --- a/routes/routes.go +++ b/routes/routes.go @@ -14,6 +14,7 @@ func NewRouter(m *models.Model) http.Handler { router.NotFound(notFoundHandler) router.Mount("/plans", newPlanRouter(m)) router.Mount("/actions", newActionRouter(m)) + router.Mount("/auth", newAuthRouter(m)) router.Mount("/health", newHealthRouter(m)) router.Get("/ping", ping) return router diff --git a/routes/users.go b/routes/users.go deleted file mode 100644 index e59bd39..0000000 --- a/routes/users.go +++ /dev/null @@ -1,36 +0,0 @@ -package routes - -import ( - "encoding/json" - "gitea.deepak.science/deepak/gogmagog/models" - "github.com/go-chi/chi" - "net/http" -) - -func newUserRouter(m *models.Model) http.Handler { - router := chi.NewRouter() - // router.Post("/", postUserFunc(m)) - router.Get("/{username}", getUserByUsernameFunc(m)) - return router -} - -func getUserByUsernameFunc(m *models.Model) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - username := chi.URLParam(r, "username") - - user, err := m.UserByUsername(username) - if err != nil { - if models.IsNotFoundError(err) { - notFoundHandler(w, r) - return - } - serverError(w, err) - return - - } - w.Header().Add("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(user); err != nil { - serverError(w, err) - } - } -}