Initial implementation of password-checking

This commit is contained in:
Ben Visness 2021-03-21 22:07:18 -05:00
parent 4fb161b3c6
commit acca4fe232
9 changed files with 206 additions and 17 deletions

1
go.mod
View File

@ -18,6 +18,7 @@ require (
github.com/stretchr/testify v1.7.0 github.com/stretchr/testify v1.7.0
github.com/teacat/noire v1.1.0 github.com/teacat/noire v1.1.0
github.com/wellington/go-libsass v0.9.2 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 replace github.com/rs/zerolog v1.20.0 => github.com/bvisness/zerolog v1.20.1-0.20210321191248-05f63bf0e9e0

8
go.sum
View File

@ -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-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-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-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-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-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-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 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-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-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-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-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-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-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/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-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-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-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/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.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.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=

69
src/auth/auth.go Normal file
View File

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

View File

@ -3,11 +3,11 @@ package db
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"reflect" "reflect"
"strings" "strings"
"git.handmade.network/hmn/hmn/src/config" "git.handmade.network/hmn/hmn/src/config"
"git.handmade.network/hmn/hmn/src/models"
"git.handmade.network/hmn/hmn/src/oops" "git.handmade.network/hmn/hmn/src/oops"
"github.com/jackc/pgx/v4" "github.com/jackc/pgx/v4"
"github.com/jackc/pgx/v4/log/zerologadapter" "github.com/jackc/pgx/v4/log/zerologadapter"
@ -61,11 +61,19 @@ func (it *StructQueryIterator) Next(dest interface{}) bool {
panic(err) panic(err)
} }
fmt.Printf("%#v\n", vals)
for i, val := range vals { for i, val := range vals {
field := v.Elem().Field(it.fieldIndices[i]) field := v.Elem().Field(it.fieldIndices[i])
switch field.Kind() { switch field.Kind() {
case reflect.Int: case reflect.Int:
field.SetInt(reflect.ValueOf(val).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: default:
field.Set(reflect.ValueOf(val)) field.Set(reflect.ValueOf(val))
} }
@ -82,7 +90,15 @@ func QueryToStructs(ctx context.Context, conn *pgxpool.Pool, destType interface{
var fieldIndices []int var fieldIndices []int
var columnNames []string 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++ { for i := 0; i < t.NumField(); i++ {
f := t.Field(i) f := t.Field(i)
if columnName := f.Tag.Get("db"); columnName != "" { if columnName := f.Tag.Get("db"); columnName != "" {

View File

@ -185,8 +185,15 @@ func LogPanicValue(logger *zerolog.Logger, val interface{}, msg string) {
} }
if err, ok := val.(error); ok { 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 { } else {
logger.Error().Interface("recovered", val).Msg(msg) logger.Error().
Interface("recovered", val).
Interface(zerolog.ErrorStackFieldName, oops.Trace()).
Msg(msg)
} }
} }

18
src/models/user.go Normal file
View File

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

View File

@ -50,7 +50,15 @@ var ZerologStackMarshaler = func(err error) interface{} {
} }
func New(wrapped error, format string, args ...interface{}) error { 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)) frames := make(CallStack, len(trace))
for i, call := range trace { for i, call := range trace {
callFrame := call.Frame() callFrame := call.Frame()
@ -61,9 +69,5 @@ func New(wrapped error, format string, args ...interface{}) error {
} }
} }
return &Error{ return frames
Message: fmt.Sprintf(format, args...),
Wrapped: wrapped,
Stack: frames,
}
} }

View File

@ -34,7 +34,11 @@ func (r *HMNRouter) GET(route string, handler HMNHandler) {
r.Handle(http.MethodGet, route, handler) 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) { func (r *HMNRouter) ServeFiles(path string, root http.FileSystem) {
r.HttpRouter.ServeFiles(path, root) r.HttpRouter.ServeFiles(path, root)
@ -94,7 +98,7 @@ func (c *RequestContext) AddErrors(errs ...error) {
c.Errors = append(c.Errors, errs...) 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.StatusCode = status
c.AddErrors(errs...) c.AddErrors(errs...)
} }

View File

@ -2,12 +2,16 @@ package website
import ( import (
"context" "context"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"io"
"net/http" "net/http"
"strings" "strings"
"git.handmade.network/hmn/hmn/src/auth"
"git.handmade.network/hmn/hmn/src/db" "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/models"
"git.handmade.network/hmn/hmn/src/oops" "git.handmade.network/hmn/hmn/src/oops"
"git.handmade.network/hmn/hmn/src/templates" "git.handmade.network/hmn/hmn/src/templates"
@ -23,7 +27,10 @@ type websiteRoutes struct {
func NewWebsiteRoutes(conn *pgxpool.Pool) http.Handler { func NewWebsiteRoutes(conn *pgxpool.Pool) http.Handler {
routes := &websiteRoutes{ routes := &websiteRoutes{
HMNRouter: &HMNRouter{HttpRouter: httprouter.New()}, HMNRouter: &HMNRouter{
HttpRouter: httprouter.New(),
Wrappers: []HMNHandlerWrapper{ErrorLoggingWrapper},
},
conn: conn, conn: conn,
} }
@ -32,6 +39,8 @@ func NewWebsiteRoutes(conn *pgxpool.Pool) http.Handler {
mainRoutes.GET("/project/:id", routes.Project) mainRoutes.GET("/project/:id", routes.Project)
mainRoutes.GET("/assets/project.css", routes.ProjectCSS) mainRoutes.GET("/assets/project.css", routes.ProjectCSS)
routes.POST("/login", routes.Login)
routes.ServeFiles("/public/*filepath", http.Dir("public")) routes.ServeFiles("/public/*filepath", http.Dir("public"))
return routes 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 { func (s *websiteRoutes) CommonWebsiteDataWrapper(h HMNHandler) HMNHandler {
return func(c *RequestContext, p httprouter.Params) { return func(c *RequestContext, p httprouter.Params) {
slug := "" slug := ""
@ -130,7 +196,7 @@ func (s *websiteRoutes) CommonWebsiteDataWrapper(h HMNHandler) HMNHandler {
dbProject, err := FetchProjectBySlug(c.Context(), s.conn, slug) dbProject, err := FetchProjectBySlug(c.Context(), s.conn, slug)
if err != nil { 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 return
} }