Adds auth route that checks username and password
All checks were successful
gitea-deepak/gogmagog/pipeline/head This commit looks good

This commit is contained in:
Deepak Mallubhotla 2021-01-12 12:43:45 -06:00
parent c8b8f87f6c
commit 262321a1e2
Signed by: deepak
GPG Key ID: 64BF53A3369104E7
9 changed files with 155 additions and 44 deletions

View File

@ -2,6 +2,7 @@ package models
import ( import (
"database/sql" "database/sql"
"golang.org/x/crypto/bcrypt"
) )
type notFoundError struct { type notFoundError struct {
@ -27,3 +28,27 @@ func wrapNotFound(err error) error {
} }
return err 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
}

View File

@ -40,3 +40,10 @@ func TestErrorModelWrapping(t *testing.T) {
_, err = m.Action(0) _, err = m.Action(0)
assert.True(models.IsNotFoundError(err)) 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))
}

View File

@ -44,7 +44,8 @@ func (ms *multiStore) SelectActionsByPlanID(plan *models.Plan) ([]*models.Action
} }
func (ms *multiStore) SelectUserByUsername(username string) (*models.User, error) { 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) { func (ms *multiStore) InsertUser(user *models.User) (int, error) {

View File

@ -22,13 +22,21 @@ type UserNoPassword struct {
DisplayName string `json:"display_name"` DisplayName string `json:"display_name"`
} }
// UserByUsername returns a single user by the unique username. // VerifyUserByUsernamePassword returns a single user by the unique username, if the provided password is correct.
func (m *Model) UserByUsername(username string) (*UserNoPassword, error) { func (m *Model) VerifyUserByUsernamePassword(username string, password string) (*UserNoPassword, error) {
user, err := m.SelectUserByUsername(username) user, err := m.SelectUserByUsername(username)
if user == nil { if err != nil {
return nil, wrapNotFound(err) // 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. // NoPassword strips the user of password.

View File

@ -17,16 +17,20 @@ func TestModelUsers(t *testing.T) {
[]*models.Plan{p}} []*models.Plan{p}}
m := models.New(ss) m := models.New(ss)
user, err := m.UserByUsername("test") user, err := m.VerifyUserByUsernamePassword("test", "password")
assert.Nil(err) assert.Nil(err)
assert.NotNil(user) assert.NotNil(user)
user, err = m.VerifyUserByUsernamePassword("test", "wrong_password")
assert.NotNil(err)
assert.Nil(user)
} }
func TestErrorUsers(t *testing.T) { func TestErrorUsers(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
m := getErrorModel(fmt.Errorf("err")) m := getErrorModel(fmt.Errorf("err"))
user, err := m.UserByUsername("snth") user, err := m.VerifyUserByUsernamePassword("snth", "aoeu")
assert.Nil(user) assert.Nil(user)
assert.NotNil(err) assert.NotNil(err)
} }

96
routes/auth.go Normal file
View File

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

View File

@ -25,3 +25,8 @@ func notFoundHandler(w http.ResponseWriter, r *http.Request) {
code := http.StatusNotFound code := http.StatusNotFound
http.Error(w, http.StatusText(code), code) 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)
}

View File

@ -14,6 +14,7 @@ func NewRouter(m *models.Model) http.Handler {
router.NotFound(notFoundHandler) router.NotFound(notFoundHandler)
router.Mount("/plans", newPlanRouter(m)) router.Mount("/plans", newPlanRouter(m))
router.Mount("/actions", newActionRouter(m)) router.Mount("/actions", newActionRouter(m))
router.Mount("/auth", newAuthRouter(m))
router.Mount("/health", newHealthRouter(m)) router.Mount("/health", newHealthRouter(m))
router.Get("/ping", ping) router.Get("/ping", ping)
return router return router

View File

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