Initial implementation of password-checking
This commit is contained in:
parent
4fb161b3c6
commit
acca4fe232
1
go.mod
1
go.mod
|
@ -18,6 +18,7 @@ require (
|
|||
github.com/stretchr/testify v1.7.0
|
||||
github.com/teacat/noire v1.1.0
|
||||
github.com/wellington/go-libsass v0.9.2
|
||||
golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670
|
||||
)
|
||||
|
||||
replace github.com/rs/zerolog v1.20.0 => github.com/bvisness/zerolog v1.20.1-0.20210321191248-05f63bf0e9e0
|
||||
|
|
8
go.sum
8
go.sum
|
@ -304,8 +304,9 @@ golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8U
|
|||
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670 h1:gzMM0EjIYiRmJI3+jBdFuoynZlpxa2JQZsolKu09BXo=
|
||||
golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
|
@ -340,8 +341,9 @@ golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn
|
|||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
|
@ -372,7 +374,9 @@ golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.handmade.network/hmn/hmn/src/oops"
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
)
|
||||
|
||||
type HashAlgorithm string
|
||||
|
||||
const (
|
||||
PBKDF2_SHA256 = "pbkdf2_sha256"
|
||||
)
|
||||
|
||||
const PKBDF2KeyLength = 64
|
||||
|
||||
type HashedPassword struct {
|
||||
Algorithm HashAlgorithm
|
||||
Iterations int
|
||||
Salt string
|
||||
Hash string
|
||||
}
|
||||
|
||||
func ParseDjangoPasswordString(s string) (HashedPassword, error) {
|
||||
pieces := strings.SplitN(s, "$", 4)
|
||||
if len(pieces) < 4 {
|
||||
return HashedPassword{}, oops.New(nil, "unrecognized password string format")
|
||||
}
|
||||
|
||||
iterations, err := strconv.Atoi(pieces[1])
|
||||
if err != nil {
|
||||
return HashedPassword{}, oops.New(err, "could not parse password iterations")
|
||||
}
|
||||
|
||||
return HashedPassword{
|
||||
Algorithm: HashAlgorithm(pieces[0]),
|
||||
Iterations: iterations,
|
||||
Salt: pieces[2],
|
||||
Hash: pieces[3],
|
||||
}, nil
|
||||
}
|
||||
|
||||
func CheckPassword(password string, hashedPassword HashedPassword) (bool, error) {
|
||||
switch hashedPassword.Algorithm {
|
||||
case PBKDF2_SHA256:
|
||||
decoded, err := base64.StdEncoding.DecodeString(hashedPassword.Hash)
|
||||
if err != nil {
|
||||
return false, oops.New(nil, "failed to get key length of hashed password")
|
||||
}
|
||||
|
||||
newHash := pbkdf2.Key(
|
||||
[]byte(password),
|
||||
[]byte(hashedPassword.Salt),
|
||||
hashedPassword.Iterations,
|
||||
len(decoded),
|
||||
sha256.New,
|
||||
)
|
||||
newHashEncoded := base64.StdEncoding.EncodeToString(newHash)
|
||||
|
||||
return bytes.Equal([]byte(newHashEncoded), []byte(hashedPassword.Hash)), nil
|
||||
default:
|
||||
return false, oops.New(nil, "unrecognized password hash algorithm: %s", hashedPassword.Algorithm)
|
||||
}
|
||||
}
|
20
src/db/db.go
20
src/db/db.go
|
@ -3,11 +3,11 @@ package db
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"git.handmade.network/hmn/hmn/src/config"
|
||||
"git.handmade.network/hmn/hmn/src/models"
|
||||
"git.handmade.network/hmn/hmn/src/oops"
|
||||
"github.com/jackc/pgx/v4"
|
||||
"github.com/jackc/pgx/v4/log/zerologadapter"
|
||||
|
@ -61,11 +61,19 @@ func (it *StructQueryIterator) Next(dest interface{}) bool {
|
|||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Printf("%#v\n", vals)
|
||||
|
||||
for i, val := range vals {
|
||||
field := v.Elem().Field(it.fieldIndices[i])
|
||||
switch field.Kind() {
|
||||
case reflect.Int:
|
||||
field.SetInt(reflect.ValueOf(val).Int())
|
||||
case reflect.Ptr:
|
||||
// TODO: I'm pretty sure we don't handle nullable ints correctly lol. Maybe this needs to be a function somehow, and recurse onto itself?? Reflection + recursion sounds like a great idea
|
||||
if val != nil {
|
||||
field.Set(reflect.New(field.Type().Elem()))
|
||||
field.Elem().Set(reflect.ValueOf(val))
|
||||
}
|
||||
default:
|
||||
field.Set(reflect.ValueOf(val))
|
||||
}
|
||||
|
@ -82,7 +90,15 @@ func QueryToStructs(ctx context.Context, conn *pgxpool.Pool, destType interface{
|
|||
var fieldIndices []int
|
||||
var columnNames []string
|
||||
|
||||
t := reflect.TypeOf(models.Project{})
|
||||
t := reflect.TypeOf(destType)
|
||||
if t.Kind() == reflect.Ptr {
|
||||
t = t.Elem()
|
||||
}
|
||||
|
||||
if t.Kind() != reflect.Struct {
|
||||
return StructQueryIterator{}, oops.New(nil, "QueryToStructs requires a struct type or a pointer to a struct type")
|
||||
}
|
||||
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
f := t.Field(i)
|
||||
if columnName := f.Tag.Get("db"); columnName != "" {
|
||||
|
|
|
@ -185,8 +185,15 @@ func LogPanicValue(logger *zerolog.Logger, val interface{}, msg string) {
|
|||
}
|
||||
|
||||
if err, ok := val.(error); ok {
|
||||
logger.Error().Err(err).Msg(msg)
|
||||
l := logger.Error().Err(err)
|
||||
if _, ok := err.(*oops.Error); !ok {
|
||||
l = l.Interface(zerolog.ErrorStackFieldName, oops.Trace())
|
||||
}
|
||||
l.Msg(msg)
|
||||
} else {
|
||||
logger.Error().Interface("recovered", val).Msg(msg)
|
||||
logger.Error().
|
||||
Interface("recovered", val).
|
||||
Interface(zerolog.ErrorStackFieldName, oops.Trace()).
|
||||
Msg(msg)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type User struct {
|
||||
ID int `db:"id"`
|
||||
|
||||
Username string `db:"username"`
|
||||
Password string `db:"password"`
|
||||
Email string `db:"email"`
|
||||
|
||||
DateJoined time.Time `db:"date_joined"`
|
||||
LastLogin *time.Time `db:"last_login"`
|
||||
|
||||
IsSuperuser bool `db:"is_superuser"`
|
||||
IsStaff bool `db:"is_staff"`
|
||||
IsActive bool `db:"is_active"`
|
||||
}
|
|
@ -50,7 +50,15 @@ var ZerologStackMarshaler = func(err error) interface{} {
|
|||
}
|
||||
|
||||
func New(wrapped error, format string, args ...interface{}) error {
|
||||
trace := stack.Trace().TrimRuntime()
|
||||
return &Error{
|
||||
Message: fmt.Sprintf(format, args...),
|
||||
Wrapped: wrapped,
|
||||
Stack: Trace(),
|
||||
}
|
||||
}
|
||||
|
||||
func Trace() CallStack {
|
||||
trace := stack.Trace().TrimRuntime()[1:]
|
||||
frames := make(CallStack, len(trace))
|
||||
for i, call := range trace {
|
||||
callFrame := call.Frame()
|
||||
|
@ -61,9 +69,5 @@ func New(wrapped error, format string, args ...interface{}) error {
|
|||
}
|
||||
}
|
||||
|
||||
return &Error{
|
||||
Message: fmt.Sprintf(format, args...),
|
||||
Wrapped: wrapped,
|
||||
Stack: frames,
|
||||
}
|
||||
return frames
|
||||
}
|
||||
|
|
|
@ -34,7 +34,11 @@ func (r *HMNRouter) GET(route string, handler HMNHandler) {
|
|||
r.Handle(http.MethodGet, route, handler)
|
||||
}
|
||||
|
||||
// TODO: POST, etc.
|
||||
func (r *HMNRouter) POST(route string, handler HMNHandler) {
|
||||
r.Handle(http.MethodPost, route, handler)
|
||||
}
|
||||
|
||||
// TODO: More methods
|
||||
|
||||
func (r *HMNRouter) ServeFiles(path string, root http.FileSystem) {
|
||||
r.HttpRouter.ServeFiles(path, root)
|
||||
|
@ -94,7 +98,7 @@ func (c *RequestContext) AddErrors(errs ...error) {
|
|||
c.Errors = append(c.Errors, errs...)
|
||||
}
|
||||
|
||||
func (c *RequestContext) AbortWithErrors(status int, errs ...error) {
|
||||
func (c *RequestContext) Errored(status int, errs ...error) {
|
||||
c.StatusCode = status
|
||||
c.AddErrors(errs...)
|
||||
}
|
||||
|
|
|
@ -2,12 +2,16 @@ package website
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"git.handmade.network/hmn/hmn/src/auth"
|
||||
"git.handmade.network/hmn/hmn/src/db"
|
||||
"git.handmade.network/hmn/hmn/src/logging"
|
||||
"git.handmade.network/hmn/hmn/src/models"
|
||||
"git.handmade.network/hmn/hmn/src/oops"
|
||||
"git.handmade.network/hmn/hmn/src/templates"
|
||||
|
@ -23,8 +27,11 @@ type websiteRoutes struct {
|
|||
|
||||
func NewWebsiteRoutes(conn *pgxpool.Pool) http.Handler {
|
||||
routes := &websiteRoutes{
|
||||
HMNRouter: &HMNRouter{HttpRouter: httprouter.New()},
|
||||
conn: conn,
|
||||
HMNRouter: &HMNRouter{
|
||||
HttpRouter: httprouter.New(),
|
||||
Wrappers: []HMNHandlerWrapper{ErrorLoggingWrapper},
|
||||
},
|
||||
conn: conn,
|
||||
}
|
||||
|
||||
mainRoutes := routes.WithWrappers(routes.CommonWebsiteDataWrapper)
|
||||
|
@ -32,6 +39,8 @@ func NewWebsiteRoutes(conn *pgxpool.Pool) http.Handler {
|
|||
mainRoutes.GET("/project/:id", routes.Project)
|
||||
mainRoutes.GET("/assets/project.css", routes.ProjectCSS)
|
||||
|
||||
routes.POST("/login", routes.Login)
|
||||
|
||||
routes.ServeFiles("/public/*filepath", http.Dir("public"))
|
||||
|
||||
return routes
|
||||
|
@ -120,6 +129,63 @@ func (s *websiteRoutes) ProjectCSS(c *RequestContext, p httprouter.Params) {
|
|||
}
|
||||
}
|
||||
|
||||
func (s *websiteRoutes) Login(c *RequestContext, p httprouter.Params) {
|
||||
bodyBytes, _ := io.ReadAll(c.Req.Body)
|
||||
|
||||
// TODO: Update this endpoint to give uniform responses on errors and to be resilient to timing attacks.
|
||||
|
||||
var body struct {
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
err := json.Unmarshal(bodyBytes, &body)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var user models.User
|
||||
err = db.QueryOneToStruct(c.Context(), s.conn, &user, "SELECT $columns FROM auth_user WHERE username = $1", body.Username)
|
||||
if err != nil {
|
||||
if errors.Is(err, db.ErrNoMatchingRows) {
|
||||
c.StatusCode = http.StatusUnauthorized
|
||||
} else {
|
||||
c.Errored(http.StatusInternalServerError, oops.New(err, "failed to look up user by username"))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
logging.Debug().Interface("user", user).Msg("the user to check")
|
||||
|
||||
hashed, err := auth.ParseDjangoPasswordString(user.Password)
|
||||
if err != nil {
|
||||
c.Errored(http.StatusInternalServerError, oops.New(err, "failed to parse password string"))
|
||||
return
|
||||
}
|
||||
|
||||
passwordsMatch, err := auth.CheckPassword(body.Password, hashed)
|
||||
if err != nil {
|
||||
c.Errored(http.StatusInternalServerError, oops.New(err, "failed to check password against hash"))
|
||||
return
|
||||
}
|
||||
|
||||
if passwordsMatch {
|
||||
c.Body.WriteString("ur good")
|
||||
} else {
|
||||
c.StatusCode = http.StatusUnauthorized
|
||||
c.Body.WriteString("nope")
|
||||
}
|
||||
}
|
||||
|
||||
func ErrorLoggingWrapper(h HMNHandler) HMNHandler {
|
||||
return func(c *RequestContext, p httprouter.Params) {
|
||||
h(c, p)
|
||||
|
||||
for _, err := range c.Errors {
|
||||
c.Logger.Error().Err(err).Msg("error occurred during request")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *websiteRoutes) CommonWebsiteDataWrapper(h HMNHandler) HMNHandler {
|
||||
return func(c *RequestContext, p httprouter.Params) {
|
||||
slug := ""
|
||||
|
@ -130,7 +196,7 @@ func (s *websiteRoutes) CommonWebsiteDataWrapper(h HMNHandler) HMNHandler {
|
|||
|
||||
dbProject, err := FetchProjectBySlug(c.Context(), s.conn, slug)
|
||||
if err != nil {
|
||||
c.AbortWithErrors(http.StatusInternalServerError, oops.New(err, "failed to fetch current project"))
|
||||
c.Errored(http.StatusInternalServerError, oops.New(err, "failed to fetch current project"))
|
||||
return
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue